mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-19 11:04:06 +00:00
git_ui: Batch merge conflict anchor resolution (#63394)
Improves merge-conflict loading by resolving ordered conflict anchors with a single excerpt sweep instead of restarting the excerpt scan four times per conflict. The pull request also adds `script/generate-merge-conflict-benchmark`, which creates validated small, medium, or severe repositories with real unresolved Git merges. On the severe workload (128 files × 256 conflicts), anchor conversion decreased from 483.5 ms to 5.9 ms, saving about 478 ms, or roughly 5% of the total refresh. Closes https://linear.app/zed-industries/issue/FR-174/improve-git-panel-performance-for-large-merge-conflicts ## Testing - `cargo test -p multi_buffer test_ordered_buffer_anchor_ranges_to_anchor_ranges` - `cargo test -p git_ui conflict` - `./script/clippy -p multi_buffer -p git_ui` - Generated and validated the small synthetic merge-conflict workload Release Notes: - Improved performance when opening large merge conflicts
This commit is contained in:
parent
e8fbacb6d7
commit
ff020dd053
4 changed files with 404 additions and 24 deletions
209
script/generate-merge-conflict-benchmark
Executable file
209
script/generate-merge-conflict-benchmark
Executable file
|
|
@ -0,0 +1,209 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
PRESETS = {
|
||||
"small": (2, 8, 16 * 1024),
|
||||
"medium": (32, 64, 128 * 1024),
|
||||
"severe": (128, 256, 512 * 1024),
|
||||
}
|
||||
|
||||
|
||||
def run_git(
|
||||
repository: Path, *arguments: str, expect_failure: bool = False
|
||||
) -> subprocess.CompletedProcess:
|
||||
environment = os.environ.copy()
|
||||
environment.update(
|
||||
{
|
||||
"GIT_CONFIG_GLOBAL": os.devnull,
|
||||
"GIT_CONFIG_SYSTEM": os.devnull,
|
||||
"GIT_AUTHOR_NAME": "Zed Benchmark",
|
||||
"GIT_AUTHOR_EMAIL": "benchmark@zed.dev",
|
||||
"GIT_COMMITTER_NAME": "Zed Benchmark",
|
||||
"GIT_COMMITTER_EMAIL": "benchmark@zed.dev",
|
||||
}
|
||||
)
|
||||
result = subprocess.run(
|
||||
["git", "-C", repository, *arguments],
|
||||
env=environment,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
if expect_failure:
|
||||
if result.returncode == 0:
|
||||
raise RuntimeError(f"git {' '.join(arguments)} unexpectedly succeeded")
|
||||
elif result.returncode != 0:
|
||||
stderr = result.stderr.decode(errors="replace").strip()
|
||||
raise RuntimeError(f"git {' '.join(arguments)} failed: {stderr}")
|
||||
return result
|
||||
|
||||
|
||||
def unchanged_separator(region_index: int) -> str:
|
||||
return "".join(
|
||||
f"// unchanged separator {region_index:04d} {line_index:02d}\n"
|
||||
for line_index in range(10)
|
||||
)
|
||||
|
||||
|
||||
def conflict_file(
|
||||
file_index: int, region_count: int, variant: str, padding: int
|
||||
) -> str:
|
||||
chunks = [
|
||||
f"// Synthetic merge-conflict workload: file {file_index:04d}, variant {variant}\n"
|
||||
]
|
||||
for region_index in range(region_count):
|
||||
value = f"{variant}-file-{file_index:04d}-region-{region_index:04d}-"
|
||||
chunks.append(
|
||||
f'pub const VALUE_{region_index:04d}: &str = "{value}{"x" * padding}";\n'
|
||||
)
|
||||
chunks.append(unchanged_separator(region_index))
|
||||
return "".join(chunks)
|
||||
|
||||
|
||||
def padding_for_target_size(file_index: int, region_count: int, target_size: int) -> int:
|
||||
unpadded_ours = conflict_file(file_index, region_count, "ours", 0)
|
||||
unpadded_theirs = conflict_file(file_index, region_count, "theirs", 0)
|
||||
marker_overhead = region_count * len(
|
||||
"<<<<<<< HEAD\n=======\n>>>>>>> conflict-side\n"
|
||||
)
|
||||
shared_separator_size = sum(
|
||||
len(unchanged_separator(region_index))
|
||||
for region_index in range(region_count)
|
||||
)
|
||||
minimum_size = (
|
||||
len(unpadded_ours)
|
||||
+ len(unpadded_theirs)
|
||||
- shared_separator_size
|
||||
+ marker_overhead
|
||||
)
|
||||
if target_size < minimum_size:
|
||||
raise ValueError(
|
||||
f"target size {target_size} is too small for {region_count} regions; "
|
||||
f"minimum is approximately {minimum_size} bytes"
|
||||
)
|
||||
return (target_size - minimum_size) // (2 * region_count)
|
||||
|
||||
|
||||
def write_variant(
|
||||
repository: Path,
|
||||
file_count: int,
|
||||
region_count: int,
|
||||
target_size: int,
|
||||
variant: str,
|
||||
) -> None:
|
||||
for file_index in range(file_count):
|
||||
path = (
|
||||
repository
|
||||
/ "conflicts"
|
||||
/ f"module_{file_index:04d}"
|
||||
/ f"generated_conflict_{file_index:04d}.rs"
|
||||
)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
padding = padding_for_target_size(file_index, region_count, target_size)
|
||||
path.write_text(
|
||||
conflict_file(file_index, region_count, variant, padding),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def validate(repository: Path, file_count: int, region_count: int) -> tuple[int, int]:
|
||||
merge_head = run_git(repository, "rev-parse", "-q", "--verify", "MERGE_HEAD")
|
||||
if not merge_head.stdout.strip():
|
||||
raise RuntimeError("MERGE_HEAD is missing")
|
||||
|
||||
unresolved_output = run_git(
|
||||
repository, "diff", "--name-only", "--diff-filter=U", "-z"
|
||||
).stdout
|
||||
unresolved_paths = [
|
||||
Path(path.decode()) for path in unresolved_output.split(b"\0") if path
|
||||
]
|
||||
if len(unresolved_paths) != file_count:
|
||||
raise RuntimeError(
|
||||
f"expected {file_count} unresolved paths, found {len(unresolved_paths)}"
|
||||
)
|
||||
|
||||
total_bytes = 0
|
||||
for relative_path in unresolved_paths:
|
||||
contents = (repository / relative_path).read_bytes()
|
||||
total_bytes += len(contents)
|
||||
marker_counts = (
|
||||
contents.count(b"<<<<<<< "),
|
||||
contents.count(b"======="),
|
||||
contents.count(b">>>>>>> "),
|
||||
)
|
||||
if marker_counts != (region_count, region_count, region_count):
|
||||
raise RuntimeError(
|
||||
f"{relative_path} has marker counts {marker_counts}, "
|
||||
f"expected {(region_count, region_count, region_count)}"
|
||||
)
|
||||
|
||||
status_entries = [
|
||||
entry
|
||||
for entry in run_git(
|
||||
repository, "status", "--porcelain=v1", "-z"
|
||||
).stdout.split(b"\0")
|
||||
if entry
|
||||
]
|
||||
if len(status_entries) != file_count or any(
|
||||
not entry.startswith(b"UU ") for entry in status_entries
|
||||
):
|
||||
raise RuntimeError("the repository contains unexpected status entries")
|
||||
|
||||
return len(unresolved_paths), total_bytes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a deterministic unresolved Git merge for Zed performance testing."
|
||||
)
|
||||
parser.add_argument("destination", type=Path)
|
||||
parser.add_argument(
|
||||
"--preset", choices=PRESETS, default="medium", help="workload size"
|
||||
)
|
||||
arguments = parser.parse_args()
|
||||
|
||||
file_count, region_count, target_size = PRESETS[arguments.preset]
|
||||
destination = arguments.destination.expanduser().resolve()
|
||||
try:
|
||||
destination.mkdir(parents=True, exist_ok=False)
|
||||
run_git(destination, "init", "-b", "main")
|
||||
|
||||
write_variant(destination, file_count, region_count, target_size, "base")
|
||||
run_git(destination, "add", "conflicts")
|
||||
run_git(destination, "commit", "-m", "base")
|
||||
|
||||
run_git(destination, "switch", "-c", "conflict-side")
|
||||
write_variant(destination, file_count, region_count, target_size, "theirs")
|
||||
run_git(destination, "add", "conflicts")
|
||||
run_git(destination, "commit", "-m", "theirs")
|
||||
|
||||
run_git(destination, "switch", "main")
|
||||
write_variant(destination, file_count, region_count, target_size, "ours")
|
||||
run_git(destination, "add", "conflicts")
|
||||
run_git(destination, "commit", "-m", "ours")
|
||||
|
||||
run_git(destination, "merge", "conflict-side", expect_failure=True)
|
||||
unresolved_count, total_bytes = validate(
|
||||
destination, file_count, region_count
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"repository: {destination}")
|
||||
print(f"preset: {arguments.preset}")
|
||||
print(f"unresolved files: {unresolved_count}")
|
||||
print(f"conflict regions per file: {region_count}")
|
||||
print(f"total conflict regions: {unresolved_count * region_count}")
|
||||
print(f"working tree bytes: {total_bytes}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue