Skip to content

Avoid getcwd when the path is already absolute#846

Open
Brett-Best wants to merge 1 commit into
jpsim:mainfrom
Brett-Best:codex/avoid-getcwd-absolute-path
Open

Avoid getcwd when the path is already absolute#846
Brett-Best wants to merge 1 commit into
jpsim:mainfrom
Brett-Best:codex/avoid-getcwd-absolute-path

Conversation

@Brett-Best

@Brett-Best Brett-Best commented Jul 17, 2026

Copy link
Copy Markdown

Avoid getcwd when the path is already absolute

What

NSString.absolutePathRepresentation(rootDirectory:) has an early return for already-absolute paths, but its default argument runs a getcwd syscall on every call regardless — even when that early return means the value is never used.

-    public func absolutePathRepresentation(rootDirectory: String = FileManager.default.currentDirectoryPath) -> String {
+    public func absolutePathRepresentation(rootDirectory: String? = nil) -> String {
         if isAbsolutePath { return expandingTildeInPath }
+        let rootDirectory = rootDirectory ?? FileManager.default.currentDirectoryPath

This makes rootDirectory optional and resolves the working directory lazily, only on the relative-path branch that actually uses it.

Why it's not as cheap as it looks

Swift evaluates a default argument at the call site, before the callee body runs. So absolutePathRepresentation() on an absolute path still computes FileManager.default.currentDirectoryPath and then discards it.

FileManager.default is a singleton, but .currentDirectoryPath is not a cached value — it does real work on every access. In swift-foundation it allocates a buffer, issues a getcwd(2) syscall, and constructs a fresh String:

https://github.com/swiftlang/swift-foundation/blob/96d4094612d02bdc6f474f73efa0695196e786e9/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift#L512-L529

(On Darwin the ObjC Foundation implementation is at least as expensive.)

Impact

File(pathDeferringReading:) calls this once per file, always with an absolute path. Tools that enumerate large projects therefore make one pointless getcwd per file during file discovery.

Profiling SwiftLint linting a ~7,000-file project (Instruments Time Profiler, release build): currentDirectoryPath accounts for ~0.8 s of CPU (≈2.7 % of the run), and it sits on the serial pre-lint file-discovery path, so it is a proportionally larger share of warm/incremental runs where little else executes. An isolated before/after A/B of the same lint measured ~3 % off both cold and warm wall-clock (the wall figure is smaller than the CPU figure because the directory walk is parallelized).

Correctness

Behavior-preserving by construction: the resolved working directory is only ever used on the relative-path branch, which is unchanged; the absolute-path branch never read it. Verified end-to-end by linting a large project with all rules enabled — identical violation output (same count, files, lines, columns) before and after.

`absolutePathRepresentation()` returns immediately for an already-absolute
path, but its default argument `FileManager.default.currentDirectoryPath`
is evaluated at every call site regardless, because Swift evaluates default
arguments before the callee runs. That property is not cached: each access
allocates a buffer, issues a `getcwd` syscall, and builds a new String
(see swift-foundation FileManager+Directories.swift), so callers that pass
absolute paths pay for it needlessly.

SourceKitten hits this once per file when constructing
`File(pathDeferringReading:)`, so tools like SwiftLint that lint thousands
of files make thousands of pointless `getcwd` calls during file discovery.
In an Instruments trace of linting a ~7,000-file project this accounts for
~0.8 s of CPU on the serial pre-lint critical path.

Make `rootDirectory` optional and resolve the working directory lazily,
only on the relative-path branch that actually uses it. The relative-path
behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Brett-Best
Brett-Best marked this pull request as ready for review July 17, 2026 06:20
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