Skip to content

fix: improve performance for large/remote workspaces#60

Merged
kmontag merged 33 commits into
mainfrom
issue-54-tramp-perf
May 5, 2026
Merged

fix: improve performance for large/remote workspaces#60
kmontag merged 33 commits into
mainfrom
issue-54-tramp-perf

Conversation

@kmontag
Copy link
Copy Markdown
Owner

@kmontag kmontag commented May 5, 2026

Cleans up unnecessary filesystem I/O and round trips to TRAMP remotes when using macher tools.

The biggest improvement is in the search and list-file tools, which now skip some defensive file-exists-p checks to avoid unnecessary O(n) filesystem (or network) access. These checks were only really relevant as a sanity check that macher--workspace-files is returning real files.

Redundant checks of file attributes, repeated calls to macher-workspace (which may run remote project-detection, a somewhat expensive operation) have also been cleaned up, and the performance of the other tools has been improved, for example the read-file tool has about 50% fewer remote interactions.

An issue with relative-path computation when receiving xref results for remote files (which come in with "local" filenames instead of TRAMP-style names) has also been fixed, and general behavior over TRAMP is now better tested.

Re-PR of #59, which was accidentally merged without squashing.

kmontag added 30 commits April 18, 2026 02:39
Add a mock remote file name handler (registered via file-name-handler-alist)
that simulates TRAMP behavior without requiring sudo, SSH, or external
infrastructure.  Uses a custom project-find-functions entry instead of
spying on macher internals.

Remote workspace tests (currently FAIL, will pass after fix):
- search produces correct relative paths over a remote connection
- search does not make O(N) remote calls for N workspace files

Nonexistent workspace file tests (currently PASS, regression guards):
- macher--tool-edit-file: errors for file in workspace-files not on disk
- macher--tool-read-file: errors for file in workspace-files not on disk
- macher--tool-search-helper: search ignores missing files gracefully
…tests

Replace the custom test-remote-project type (which used cl-defmethod to
define project-root and project-files specializations) with a real git
repo discovered by project-vc through the mock file handler. This tests
the actual project.el code path that TRAMP-based projects use.

Changes:
- Remove cl-defmethod definitions for test-remote-project
- Initialize a git repo in the temp directory instead
- Remove project-find-functions manipulation
- Let project-vc discover the project naturally through the mock handler
- In the O(N) test, git-add the extra files instead of overriding
  project-find-functions
Three fixes for poor performance and broken behavior when the workspace
is a remote TRAMP directory:

1. macher--project-files: use the local part of project-id for
   file-relative-name, since project-files returns paths without the
   TRAMP remote prefix.

2. macher--search-get-xref-matches: remove per-file file-exists-p
   checks that cause O(N) remote round-trips.  Workspace files come
   from project-files and are expected to exist; non-existent files
   produce no grep matches and context-deleted files are filtered later.

3. macher--search-get-xref-matches: relativize xref result paths
   against the local part of workspace-root, since xref returns paths
   without the remote prefix.
…lve-workspace-path

Consolidate redundant file I/O calls that each become a remote
round-trip over TRAMP:

- resolve-workspace-path: use a single file-attributes call per path
  component instead of separate file-exists-p + file-symlink-p +
  file-directory-p (3 round-trips → 1).

- tool-read-file: inline content fetching with the already-resolved
  path instead of going through with-workspace-file which resolves the
  path a second time.  Also avoid calling file-symlink-p twice.

- with-workspace-file: remove redundant resolve-workspace-path call
  from the get-or-create-file-contents lambda.

- tool-list-directory: use a single file-attributes call per directory
  entry instead of separate file-exists-p + file-symlink-p +
  file-directory-p.  Reuse the attrs for symlink target info.

Benchmarked with 50ms simulated latency over SSH to localhost:
  read-file:      6.3s → 3.1s (51% faster, 92 → 34 I/O calls)
  list-directory: 2.0s → 1.7s (18% faster, 43 → 35 I/O calls)
The previous approach used file-remote-p 'localname on workspace-root
to compensate for xref returning local paths.  This doesn't match real
TRAMP behavior (where xref returns fully-prefixed remote paths) and was
only needed for the mock test handler.

Fix the search result relativization to strip remote prefixes from
BOTH the original-file and workspace-root before calling
file-relative-name.  This handles all cases correctly:
- Real TRAMP: both have prefix → both stripped → correct relative path
- Mock: only workspace-root has prefix → workspace-root stripped → correct
- Local: neither has prefix → unchanged → correct

Also remove the incorrect localname workaround from project-files,
fix the mock handler's file-relative-name to not special-case mismatched
prefixes, and update test comments.
- read-file: use :to-equal with exact 'Symlink target: <path>' string
  instead of two separate partial :to-match checks
- list-directory: check full 'link: <name> -> <target>' including the
  symlink target path, not just the prefix
Previously macher--workspace-root called file-directory-p and
macher--project-root called project-current + file-directory-p on every
invocation.  Each of these is a remote round-trip over TRAMP, and
project-current also triggers project-try-vc which walks the directory
tree probing for .git and .gitmodules.

These validations are dropped: any real file operation downstream will
fail with a reasonable error if the root doesn't exist, and project-files
re-validates the project when it's actually needed.

Also adds an optional ROOT-PATH argument to macher--workspace-files so
that callers with an already-resolved root can avoid a redundant
macher--workspace-root call, and passes it through from
macher--resolve-workspace-path.
With file-directory-p removed from macher--workspace-root and
project-current removed from macher--project-root, workspace-root is
now free of remote I/O.  Calling it multiple times in one tool
invocation is no longer a concern, so the optional ROOT-PATH argument
added to avoid redundant calls is not worth the signature change.

Also updates the workspace-root test to verify the real property of
interest (no remote I/O during resolution) rather than an
implementation-detail call count.
Two independent optimizations:

- macher--tool-search: compute workspace-files once and pass it to
  macher--resolve-workspace-path instead of letting both sites call
  macher--workspace-files separately.  Each call transitively triggers
  project-current (via macher--project-files), which walks the directory
  tree probing for .git and .gitmodules — expensive over TRAMP.

- macher-context--contents-for-file: drop the file-exists-p check
  before insert-file-contents.  The two operations are separate remote
  round-trips; instead, try to read and treat a read failure as a
  non-existent file.

Adds regression tests for both properties.
Same pattern as the search fix: macher--tool-list-directory called
macher--workspace-files twice (once transitively via
macher--resolve-workspace-path, once directly in collect-entries).
Each call transitively triggers project-current, which walks the
directory tree probing for a VC root — expensive over TRAMP.

Now the workspace-files list is computed once at the top of
list-directory and reused for both path resolution and entry
collection.

Adds a regression test.
Catching the broader file-error signal in macher-context--contents-for-file
would silently convert permission-denied / TRAMP connection failures into
"file doesn't exist", hiding real errors from callers.  Narrow the catch to
file-missing (still avoids the extra existence-probe round-trip) and let
other file-error subtypes propagate.

Also add unit tests for:
- the file-error propagation behavior, and
- search tolerating stale workspace-files entries that point at files no
  longer present on disk.
Drop the broader file-error arm so real errors (permission denied,
TRAMP connection failure, etc.) propagate to callers instead of being
silently converted to "file doesn't exist".
kmontag added 3 commits May 2, 2026 00:38
`gptel-make-preset` mutates `gptel--known-presets` via `nconc`. The
inline-presets describe in test-functional.el captured the list head
into `original-gptel--known-presets` by reference, so when its
`before-each` ran `macher-install` (which calls `gptel-make-preset`),
the captured "original" was mutated alongside the live list. The
`after-each` `(setq gptel--known-presets original-gptel--known-presets)`
was then a no-op, leaving macher presets registered globally for
subsequent test files.

The leak surfaces in the integration tests' default-before-action
suite, where `macher--before-action-insert-prompt` finds `macher-ro`
in `gptel--known-presets` and inserts `@macher-ro` into the action
buffer prompt, breaking the expected buffer content.

Capture a copy of the list head so the restoration actually undoes
`macher-install`.

Change-Id: I56f00283caa40bcc7f843ae6c962c3daf19aee90
* macher.el (macher--transform-system-replace-placeholder)
(macher-abort): Use the no-binding form of `when-let*' for
buffer-live-p gates instead of binding to a placeholder, which
recent byte compilers flag as 'variable not left unused'.

* macher.el (macher--resolve-workspace-path)
(macher--tool-list-directory): Drop double-negation when checking
for non-nil `file-attributes' results.

Change-Id: Ifdf8c88bfea20b56df9f9c731275e301c56ba39e
@kmontag kmontag merged commit 2933848 into main May 5, 2026
8 checks passed
@kmontag kmontag deleted the issue-54-tramp-perf branch May 5, 2026 20:03
kmontag added a commit to yantar92/macher that referenced this pull request May 6, 2026
Brings in the directory workspace type (kmontag#48), no-workspace user-error
fix (kmontag#48), and the large/remote workspace perf fixes (kmontag#60).

Resolved conflicts:
- macher.el (macher--tool-read-file): kept main's perf-improved body
  that skips the redundant path resolution in macher--with-workspace-file,
  but used the new `macher--check-output-length' helper from this branch
  in place of main's inline length check.
- tests/test-unit.el (macher--tool-search-helper): kept both `it'
  blocks added at the same spot (one from each side).

Change-Id: I713522bc98a021f55cf186806c2f34de3d5592a3
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