Skip to content

Conversation

landaire
Copy link

@landaire landaire commented Oct 10, 2025

After filing #7685 I ran some perf traces to try to understand just what was taking so long during these slow operations. The changes in this PR reduces clone time for my large repo from about 10 minutes to 4m30s.

You can see my thought process in the comments of the above task but to summarize:

During checkout we check files/directories being created to ensure that we are not attempting to write to a reserved directory (.jj/, .git/). same_file::is_same_file() is an expensive check that invokes at least 4 syscalls when called in a naive manner (open() and close() for each path -- plus possibly more for getting file info? I haven't counted).

There are a few optimization gaps here that are causing significant slowdowns. The following checklist reflects what I've optimized in this PR, and what still remains:

  • create_parent_dirs will be called for each file/directory and for each parent dir in a path try to create it and check if the dir is an illegal name via reject_reserved_existing_path(). There is no caching of directories which have already been created.
  • reject_reserved_existing calls same_file::is_same_file() in a loop for all reserved names, but the path which has maybe been created isn't going to change, so its handle could be cached.
  • can_create_new_file attempts to create the file then just uses the result as an indicator of whether or not the file is created. However, since we have a File that File can be directly converted to a same_file::Handle and avoid a syscall that currently occurs when converting the Path to a same_file::Handle.
  • can_create_new_file deletes the file immediately after. There's probably an opportunity here to not delete the file and re-use it for file write operations.
  • Say we have 1000 files in foo/. For each file that's written, reject_reserved_existing is going to make at least RESERVED_DIR_NAMES.len() * 1000 syscalls constructing foo/{reserved_dir_name} paths, testing their existence, etc. Maybe jj might create this dir? But I don't think that should ever happen -- so why not cache the handle if it's created and use a lookup table in reject_reserved_existing to only conduct these types of checks if the handle is resolved? Or alternatively cache that the file does not exist after the first check.

Here are some perf traces of running a jj git clone of my large repo before:

Release: https://share.firefox.dev/4oiSTBw
Debug: https://share.firefox.dev/4qmJBX1

And after:

Release: https://share.firefox.dev/4nK66mH
Debug: https://share.firefox.dev/470W1ed

Checklist

If applicable:

  • I have updated CHANGELOG.md
  • I have updated the documentation (README.md, docs/, demos/)
  • I have updated the config schema (cli/src/config-schema.json)
  • I have added/updated tests to cover my changes

@landaire landaire requested a review from a team as a code owner October 10, 2025 20:03
@landaire landaire force-pushed the checkout-perf-improvements branch 5 times, most recently from 0f82ded to bb26e3a Compare October 10, 2025 21:09
@landaire landaire changed the title checkout: substantially reduce syscalls lib: substantially reduce syscalls during checkout Oct 10, 2025
@martinvonz
Copy link
Member

Thanks! Could you move/copy the PR description to the commit message? See https://jj-vcs.github.io/jj/latest/contributing/

@landaire landaire force-pushed the checkout-perf-improvements branch from bb26e3a to 60e30f9 Compare October 10, 2025 21:28
@landaire
Copy link
Author

Done. Sorry about that.

@landaire
Copy link
Author

landaire commented Oct 11, 2025

I wasn't planning on it, but I addressed the last two optimization opportunities. The changes in this PR right now make my original scenario go from 10 minutes to 4.5, and with the changes applied in this commit I've shaved off another ~30s: landaire-contrib/jj@checkout-perf-improvements...landaire-contrib:jj:push-uvunuzyowqrw

Please let me know if you would like me to merge it into this commit or in a separate PR. I think I need to sleep on it and clean up the code a little bit, I'm unsure if I'm happy with how that commit is right now.

New debug trace: https://share.firefox.dev/4hfSltF.
Release: https://share.firefox.dev/46MxHxO

You'll notice in particular that reject_reserved_existing* is basically gone from the debug flamegraph, and can_create_new_file never calls CloseHandle.

I'll also note that this is still slower than a git clone which runs at 2 min 1s, which is probably expected since the repo is colocated.

@landaire landaire force-pushed the checkout-perf-improvements branch 2 times, most recently from 46413d8 to 6a526f5 Compare October 11, 2025 23:39
// In practice, none of these should panic.

// Strip working_copy_path from path_prefix to get the common repo prefix
let common_repo_prefix = path_prefix
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please advise on these .expect() calls. Happy to convert to a proper error if you can recommend an error type, but I don't think these will ever panic in practice.

@landaire landaire force-pushed the checkout-perf-improvements branch 5 times, most recently from b1ab59f to c62e9df Compare October 12, 2025 17:48
Copy link
Contributor

@yuja yuja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR includes two separate changes and touches security code which we should review carefully, I suggest splitting it into "reuse open file" and "reuse previously-created directory" patches. Thanks.

err: err.into(),
}),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can instead add same_file_handle_from_file()/same_file_handle_from_path() helpers? Since "from_file" shouldn't fail with NotFound error, we can make it a bit stricter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's continue this discussion on #7742

@landaire landaire changed the title lib: substantially reduce syscalls during checkout lib: substantially reduce syscalls when attempting to create dirs Oct 16, 2025
@landaire landaire force-pushed the checkout-perf-improvements branch from c62e9df to a37b821 Compare October 16, 2025 01:19
landaire added a commit to landaire-contrib/jj that referenced this pull request Oct 16, 2025
See jj-vcs#7695 for motivation/background

One of the hottest paths in `jj` during checkout is `can_create_file`
and `create_parent_dirs`. Both of these functions check to ensure that
there aren't attempts to write to special directories
(`reject_reserved_existing`) which is a fairly expensive check.

This change reduces file open/close syscalls by re-busing file handles
when possible and caching paths where we know a reserved directory does not exist.
landaire added a commit to landaire-contrib/jj that referenced this pull request Oct 16, 2025
See jj-vcs#7695 for motivation/background

One of the hottest paths in `jj` during checkout is `can_create_file`
and `create_parent_dirs`. Both of these functions check to ensure that
there aren't attempts to write to special directories
(`reject_reserved_existing`) which is a fairly expensive check.

This change reduces file open/close syscalls by reusing file handles
when possible and caching paths where we know a reserved directory does not exist.
After filing jj-vcs#7685 I ran some perf traces to try to understand just what was taking so long during these slow operations. The changes in this PR reduces clone time for my large repo from about 10 minutes to 4m30s.

You can see my thought process in the comments of the above task but to summarize:

During checkout we check files/directories being created to ensure that we are not attempting to write to a reserved directory (`.jj/`, `.git/`). `same_file::is_same_file()` is an expensive check that invokes _at least 4_ syscalls when called in a naive manner (`open()` and `close()` for each path -- plus possibly more for getting file info? I haven't counted).

There are a few optimization gaps here that are causing significant slowdowns. The following checklist reflects what I've optimized in this PR, and what still remains:

- [x] `create_parent_dirs` will be called for each file/directory and for each parent dir in a path **try to create it and check if the dir is an illegal name via `reject_reserved_existing_path()`**. There is no caching of directories which have already been created.
- [ ] `reject_reserved_existing` calls `same_file::is_same_file()` in a loop for all reserved names, but the path which _has maybe been created_ isn't going to change, so its handle could be cached.
- [ ] `can_create_new_file` attempts to create the file then just uses the result as an indicator of whether or not the file is created. However, since we _have a `File`_ that `File` can be directly converted to a `same_file::Handle` and avoid a syscall that currently occurs when converting the `Path` to a `same_file::Handle`.
- [ ] `can_create_new_file` deletes the file immediately after. There's probably an opportunity here to **not** delete the file and re-use it for file write operations.
- [ ] Say we have 1000 files in `foo/`. For each file that's written, `reject_reserved_existing` is going to make at least `RESERVED_DIR_NAMES.len() * 1000` syscalls constructing `foo/{reserved_dir_name}` paths, testing their existence, etc. Maybe `jj` might create this dir? But I don't think that should ever happen -- so why not cache the handle **if** it's created and use a lookup table in `reject_reserved_existing` to only conduct these types of checks if the handle is resolved? Or alternatively cache that the file _does not_ exist after the first check.

Here are some perf traces of running a `jj git clone` of my large repo before:

Release: https://share.firefox.dev/4oiSTBw
Debug: https://share.firefox.dev/4qmJBX1

And after:

Release: https://share.firefox.dev/4nK66mH
Debug: https://share.firefox.dev/470W1ed
@landaire landaire force-pushed the checkout-perf-improvements branch from a37b821 to 7b4b7b5 Compare October 16, 2025 02:35
landaire added a commit to landaire-contrib/jj that referenced this pull request Oct 17, 2025
checkout

See jj-vcs#7695 for motivation/background

One of the hottest paths in `jj` during checkout is `can_create_file`
and `create_parent_dirs`. Both of these functions check to ensure that
there aren't attempts to write to special directories
(`reject_reserved_existing`) which is a fairly expensive check.

This change reduces file open/close syscalls by reusing file handles
when possible and caching paths where we know a reserved directory does not exist.
landaire added a commit to landaire-contrib/jj that referenced this pull request Oct 17, 2025
checkout

See jj-vcs#7695 for motivation/background

One of the hottest paths in `jj` during checkout is `can_create_file`
and `create_parent_dirs`. Both of these functions check to ensure that
there aren't attempts to write to special directories
(`reject_reserved_existing`) which is a fairly expensive check.

This change reduces file open/close syscalls by reusing file handles
when possible and caching paths where we know a reserved directory does not exist.
landaire added a commit to landaire-contrib/jj that referenced this pull request Oct 17, 2025
checkout

See jj-vcs#7695 for motivation/background

One of the hottest paths in `jj` during checkout is `can_create_file`
and `create_parent_dirs`. Both of these functions check to ensure that
there aren't attempts to write to special directories
(`reject_reserved_existing`) which is a fairly expensive check.

This change reduces file open/close syscalls by reusing file handles
when possible and caching paths where we know a reserved directory does not exist.
landaire added a commit to landaire-contrib/jj that referenced this pull request Oct 17, 2025
checkout

See jj-vcs#7695 for motivation/background

One of the hottest paths in `jj` during checkout is `can_create_file`
and `create_parent_dirs`. Both of these functions check to ensure that
there aren't attempts to write to special directories
(`reject_reserved_existing`) which is a fairly expensive check.

This change reduces file open/close syscalls by reusing file handles
when possible and caching paths where we know a reserved directory does not exist.
landaire added a commit to landaire-contrib/jj that referenced this pull request Oct 17, 2025
checkout

See jj-vcs#7695 for motivation/background

One of the hottest paths in `jj` during checkout is `can_create_file`
and `create_parent_dirs`. Both of these functions check to ensure that
there aren't attempts to write to special directories
(`reject_reserved_existing`) which is a fairly expensive check.

This change reduces file open/close syscalls by reusing file handles
when possible.
landaire added a commit to landaire-contrib/jj that referenced this pull request Oct 18, 2025
checkout

See jj-vcs#7695 for motivation/background

One of the hottest paths in `jj` during checkout is `can_create_file`
and `create_parent_dirs`. Both of these functions check to ensure that
there aren't attempts to write to special directories
(`reject_reserved_existing`) which is a fairly expensive check.

This change reduces file open/close syscalls by reusing file handles
when possible.
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.

3 participants