fs: make the Into<Path> conversion of std/fs cheaper, lower 19 allocation budgets (step 2d of #68) #728

Closed
opened 2026-11-13 00:56:37 +00:00 by orion · 3 comments
Collaborator

Context

Step 2d of the epic #68 (PATH.md slice 2). Since std/fs takes Into<Path> (#547, landed 2026-10-28) every call builds a Path first: String is parsed into a root plus a component list, and the wrapper then renders it again with show() (textOf in std/fs/src/lib.trb), or the native takes the Into<Path> parameter and converts it once. Programs that call std/fs therefore allocate more blocks. The regression was found by tier B on main (issue #550) and the 19 .allocations budgets were raised in PR #556 ("making the Into<Path> conversion cheaper ... would lower the budgets again" was named there as not done). This issue does that.

Source

  • PATH.md section 2 (From<String> is infallible), section 5 (Into<Path> as a parameter type), section 6 (the boundary to the operating system, systemPath); slice 2 in section 8.
  • Numbers: compiler/CONTRIBUTING.md (tier B holds .allocations exactly on linux x86_64; a count below the budget passes and names it; lower with sh tools/conformance.sh --update-budgets --filter <program>; "Measure a std/ body with a compiled probe"), docs/PERFORMANCE.md section 6.
  • #550 / PR #556 (the raise), #68 (epic), #709 (tier B for std/ pull requests).

The numbers (budget before step 2 -> raised in PR #556 -> on main today)

program before PR #556 today
absolute-path-form 30 155 163
collection-natives 100 148 163
current-process 104 137 147
encoding-numbers-and-text 2034 2038 2085
encoding-round-trip 2796 2797 2888
file-atomic-replace 326 1615 1790
file-bytes 253 780 847
file-scope-end 49 191 207
file-streams 441 463 474
file-values 44 186 202
files 78 182 197
io-eight-files 273667 273841 273898
long-paths 36 206 224
native-into-path 299 554 612
non-ascii-paths 47 237 263
process-non-ascii-argument 55 125 135
resource-bytes 89 335 351
resource-missing 217 504 525
resource-text 144 267 276

The "today" column is the file on main at the time of filing (it moved after #556 through other changes); the target below is stated against "before", the measured count is what tier B prints.

Design (decided by the loop, internal)

  • Measure first, in forge CI, with a compiled probe (CONTRIBUTING): where do the blocks of one File.readText("a/b") go (parse of the String, the component list and its strings, show() of the result, the copy for the native)? Record the per-call count in the PR.
  • Then cut the conversion on the paths the numbers name. Candidate designs, in order of preference, the worker picks by the measurement: (1) skip the round trip for a String or literal argument whose text is already the / form (the common case) so the String goes to the runtime without building a Path; (2) one conversion at the entry of a wrapper instead of one per helper (textOf once, the helpers take the String); (3) a cheaper Path.from (fewer intermediate strings and lists). Semantics stay exactly those of PATH.md: \ separates, the ./../empty rules, IoError.path and list/absolutePath return types unchanged (steps 3 and 4).
  • A change that would make a Path argument cost more than today is not taken.

Acceptance criteria

  • A compiled probe (committed under tests/ or described with its counts in the PR body) shows the blocks per call of the conversion before and after.
  • The budgets of absolute-path-form, file-atomic-replace, files, file-values, long-paths, non-ascii-paths, resource-missing and native-into-path are back at or within 25 percent of their "before" value in the table (for example absolute-path-form at most 38, file-atomic-replace at most 408); where a program cannot get there, the PR body names the measured floor and why, and the budget is lowered to that count.
  • The other eleven budgets of the table are lowered to the count tier B prints, with sh tools/conformance.sh --update-budgets --filter <program>, never raised.
  • No other .allocations budget rises; tier B is green. The numbers are read from forge CI only (tier B on linux x86_64), never from a local run.
  • std/fs semantics are unchanged: the std/fs and std/path tests and the conformance programs native-into-path, non-ascii-paths, long-paths pass without edits to their expected output.

Seed

Does not touch the seed on its own: no syntax, no new native, no driver flag, no name the compiler looks up. If the chosen design needs a lowering change in compiler/src (design 1 may), it must work with the published seed, otherwise stop and comment here; a seed-touching variant becomes the two-commit rule and a separate issue.

Size

One PR, one or two Conventional Commits: perf(fs): convert Into<Path> arguments without a second Path and test(conformance): lower the allocation budgets of the programs that call std/fs (budget lowering as a commit of its own, with the reason in the message).

CI note

A pull request that touches only std/ and tests/ does not run tier B (tier-b scope in ci.yml, see #709), and the budgets are exact. The worker checks the scope; if the PR run has no tier B, the counts come from the tier B of the main run after landing and any difference is fixed forward in the next commit, as CONTRIBUTING says. Do not guess the counts.

Dependencies

None open. Independent of #680 (step 3); both touch std/fs, so the later one rebases.

Part of #68 (the PR says Refs #68, not Closes).

## Context Step 2d of the epic #68 (PATH.md slice 2). Since `std/fs` takes `Into<Path>` (#547, landed 2026-10-28) every call builds a `Path` first: `String` is parsed into a root plus a component list, and the wrapper then renders it again with `show()` (`textOf` in `std/fs/src/lib.trb`), or the native takes the `Into<Path>` parameter and converts it once. Programs that call `std/fs` therefore allocate more blocks. The regression was found by tier B on `main` (issue #550) and the 19 `.allocations` budgets were raised in PR #556 ("making the `Into<Path>` conversion cheaper ... would lower the budgets again" was named there as not done). This issue does that. ## Source - [PATH.md section 2](https://git.torb.dev/torbscript/language/src/branch/main/docs/design/PATH.md) (`From<String>` is infallible), section 5 (`Into<Path>` as a parameter type), section 6 (the boundary to the operating system, `systemPath`); slice 2 in section 8. - Numbers: `compiler/CONTRIBUTING.md` (tier B holds `.allocations` exactly on linux x86_64; a count below the budget passes and names it; lower with `sh tools/conformance.sh --update-budgets --filter <program>`; "Measure a `std/` body with a compiled probe"), `docs/PERFORMANCE.md` section 6. - #550 / PR #556 (the raise), #68 (epic), #709 (tier B for `std/` pull requests). ## The numbers (budget before step 2 -> raised in PR #556 -> on `main` today) | program | before | PR #556 | today | |---|---|---|---| | absolute-path-form | 30 | 155 | 163 | | collection-natives | 100 | 148 | 163 | | current-process | 104 | 137 | 147 | | encoding-numbers-and-text | 2034 | 2038 | 2085 | | encoding-round-trip | 2796 | 2797 | 2888 | | file-atomic-replace | 326 | 1615 | 1790 | | file-bytes | 253 | 780 | 847 | | file-scope-end | 49 | 191 | 207 | | file-streams | 441 | 463 | 474 | | file-values | 44 | 186 | 202 | | files | 78 | 182 | 197 | | io-eight-files | 273667 | 273841 | 273898 | | long-paths | 36 | 206 | 224 | | native-into-path | 299 | 554 | 612 | | non-ascii-paths | 47 | 237 | 263 | | process-non-ascii-argument | 55 | 125 | 135 | | resource-bytes | 89 | 335 | 351 | | resource-missing | 217 | 504 | 525 | | resource-text | 144 | 267 | 276 | The "today" column is the file on `main` at the time of filing (it moved after #556 through other changes); the target below is stated against "before", the measured count is what tier B prints. ## Design (decided by the loop, internal) - Measure first, in forge CI, with a compiled probe (CONTRIBUTING): where do the blocks of one `File.readText("a/b")` go (parse of the `String`, the component list and its strings, `show()` of the result, the copy for the native)? Record the per-call count in the PR. - Then cut the conversion on the paths the numbers name. Candidate designs, in order of preference, the worker picks by the measurement: (1) skip the round trip for a `String` or literal argument whose text is already the `/` form (the common case) so the `String` goes to the runtime without building a `Path`; (2) one conversion at the entry of a wrapper instead of one per helper (`textOf` once, the helpers take the `String`); (3) a cheaper `Path.from` (fewer intermediate strings and lists). Semantics stay exactly those of PATH.md: `\` separates, the `.`/`..`/empty rules, `IoError.path` and `list`/`absolutePath` return types unchanged (steps 3 and 4). - A change that would make a `Path` argument cost more than today is not taken. ## Acceptance criteria - [ ] A compiled probe (committed under `tests/` or described with its counts in the PR body) shows the blocks per call of the conversion before and after. - [ ] The budgets of `absolute-path-form`, `file-atomic-replace`, `files`, `file-values`, `long-paths`, `non-ascii-paths`, `resource-missing` and `native-into-path` are back at or within 25 percent of their "before" value in the table (for example `absolute-path-form` at most 38, `file-atomic-replace` at most 408); where a program cannot get there, the PR body names the measured floor and why, and the budget is lowered to that count. - [ ] The other eleven budgets of the table are lowered to the count tier B prints, with `sh tools/conformance.sh --update-budgets --filter <program>`, never raised. - [ ] No other `.allocations` budget rises; tier B is green. The numbers are read from forge CI only (tier B on linux x86_64), never from a local run. - [ ] `std/fs` semantics are unchanged: the `std/fs` and `std/path` tests and the conformance programs `native-into-path`, `non-ascii-paths`, `long-paths` pass without edits to their expected output. ## Seed Does not touch the seed on its own: no syntax, no new native, no driver flag, no name the compiler looks up. If the chosen design needs a lowering change in `compiler/src` (design 1 may), it must work with the published seed, otherwise stop and comment here; a seed-touching variant becomes the two-commit rule and a separate issue. ## Size One PR, one or two Conventional Commits: `perf(fs): convert Into<Path> arguments without a second Path` and `test(conformance): lower the allocation budgets of the programs that call std/fs` (budget lowering as a commit of its own, with the reason in the message). ## CI note A pull request that touches only `std/` and `tests/` does not run tier B (`tier-b` scope in `ci.yml`, see #709), and the budgets are exact. The worker checks the scope; if the PR run has no tier B, the counts come from the tier B of the `main` run after landing and any difference is fixed forward in the next commit, as CONTRIBUTING says. Do not guess the counts. ## Dependencies None open. Independent of #680 (step 3); both touch `std/fs`, so the later one rebases. Part of #68 (the PR says `Refs #68`, not `Closes`).
Author
Collaborator

[loop:torbscript] decided by the loop: performance work with a measurable goal, so the loop decides (contract: performance, measurable); no owner question. Reasoning: (1) the regression is exact and known (19 budgets, table in the body, PR #556 raised them and named this follow-up); (2) the acceptance criteria are counts printed by tier B, with a probe first, so the worker measures before choosing among the three designs; (3) no seed impact as filed, a seed-touching variant stops and splits; (4) size one PR, one or two commits. Duplicate search of open and closed issues (Into, allocation budget, conversion cheaper, Path conversion, std/fs): none; #566 (ArrayList over Buffer allocations) is unrelated, #709 (tier B for std/ pull requests) is a neighbour and is noted in the issue. Labels: loop, ready.

[loop:torbscript] decided by the loop: performance work with a measurable goal, so the loop decides (contract: performance, measurable); no owner question. Reasoning: (1) the regression is exact and known (19 budgets, table in the body, PR #556 raised them and named this follow-up); (2) the acceptance criteria are counts printed by tier B, with a probe first, so the worker measures before choosing among the three designs; (3) no seed impact as filed, a seed-touching variant stops and splits; (4) size one PR, one or two commits. Duplicate search of open and closed issues (Into<Path>, allocation budget, conversion cheaper, Path conversion, std/fs): none; #566 (ArrayList over Buffer allocations) is unrelated, #709 (tier B for std/ pull requests) is a neighbour and is noted in the issue. Labels: loop, ready.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261113 until 2026-11-13T07:00:00Z

[loop:torbscript] claim implement by session:loop-torbscript-20261113 until 2026-11-13T07:00:00Z
Author
Collaborator

[loop:torbscript] release implement: worker timed out after 6 h with no report; PR #729 exists (head 2850b61e), CI pending. next: waiting (PR #729 CI), fix round on the same branch if it goes red

[loop:torbscript] release implement: worker timed out after 6 h with no report; PR #729 exists (head 2850b61e), CI pending. next: waiting (PR #729 CI), fix round on the same branch if it goes red
argus closed this issue 2026-11-13 08:25:57 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#728
No description provided.