perf(git): fix slow diff cutting around line - #38626
Conversation
Avoid regex compilation/matching and named capture group map allocations. Replace bufio.Scanner string allocations with on-the-fly byte slice scanning and stop parsing early when currentLine > line. This yields a 3.0x speedup. Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR optimizes CutDiffAroundLine in modules/git/diff.go by replacing the regex-based hunk header parsing and bufio.Scanner loop with a byte-slice based parser and manual line scanning, aiming to reduce CPU and allocations when cutting diffs for comments/reviews.
Changes:
- Replaced regexp-based hunk header parsing with
parseHunkHeaderBytes. - Reworked diff scanning from
bufio.Scannerto a byte-slice + offset approach. - Rebuilt output via
strings.Builderusing recorded line offsets instead of accumulating strings.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| oldSubparts := strings.SplitN(oldPart, ",", 2) | ||
| var err error | ||
| beginOld, err = strconv.ParseInt(oldSubparts[0], 10, 64) | ||
| if err != nil { | ||
| return 0, 0, 0, 0, false | ||
| } | ||
| if len(oldSubparts) > 1 { | ||
| endOld, err = strconv.ParseInt(oldSubparts[1], 10, 64) | ||
| if err != nil { | ||
| return 0, 0, 0, 0, false | ||
| } | ||
| } | ||
|
|
||
| newSubparts := strings.SplitN(newPart, ",", 2) | ||
| beginNew, err = strconv.ParseInt(newSubparts[0], 10, 64) | ||
| if err != nil { | ||
| return 0, 0, 0, 0, false | ||
| } | ||
| if len(newSubparts) > 1 { | ||
| endNew, err = strconv.ParseInt(newSubparts[1], 10, 64) | ||
| if err != nil { | ||
| return 0, 0, 0, 0, false | ||
| } | ||
| } |
| const cmdDiffHead = "diff --git " | ||
|
|
||
| func isHeader(lof string, inHunk bool) bool { | ||
| return strings.HasPrefix(lof, cmdDiffHead) || (!inHunk && (strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++"))) | ||
| func isHeader(lof []byte, inHunk bool) bool { | ||
| return bytes.HasPrefix(lof, []byte(cmdDiffHead)) || (!inHunk && (bytes.HasPrefix(lof, []byte("---")) || bytes.HasPrefix(lof, []byte("+++")))) | ||
| } |
| oldPart := string(parts[0]) | ||
| newPart := string(parts[1]) | ||
|
|
||
| oldSubparts := strings.SplitN(oldPart, ",", 2) | ||
| var err error |
| otherLine = beginOld | ||
| } | ||
|
|
||
| end += begin // end is for real only the number of lines in hunk |
|
|
||
| scanner := bufio.NewScanner(originalDiff) | ||
| hunk := make([]string, 0) | ||
| data, err := io.ReadAll(originalDiff) |
|
It is never the key performance point on the hot path. If you'd like to optimize, I'd like to suggest you to profile the slow endpoints first, catch the real problem. But not just try to optimize everything by "feeling". |
|
Google this: Premature optimization is the root of all evil. |
| nextLine := func() (int, int, bool) { | ||
| if pos >= len(data) { | ||
| return 0, 0, false | ||
| } | ||
| start := pos | ||
| idx := bytes.IndexByte(data[pos:], '\n') | ||
| if idx == -1 { | ||
| pos = len(data) | ||
| return start, len(data), true | ||
| } | ||
| pos += idx + 1 | ||
| return start, start + idx, true | ||
| } |
wxiaoguang
left a comment
There was a problem hiding this comment.
- it is questionable how much performance improvement it would bring in production
- not right
Optimize CutDiffAroundLine using a stream-safe rolling ring-buffer. Replaces regexp-based hunk header parsing with a fast byte-slice parser, and reuses pre-allocated slice buffers to drop heap allocations for discarded lines. This avoids memory regressions on large files, keeping memory footprint constant. Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
|
Thank you both @lunny and @wxiaoguang for your reviews and feedback. I completely agree with the points raised:
To address the memory regression while keeping the performance improvements, I have refactored the code to use a stream-safe rolling ring-buffer inside the original At this stage, I would like to offer two options depending on what the maintainers prefer:
Let me know how you'd like to proceed |
|
My judgment standards:
So the current PR's code doesn't meet any of my standards. |
|
Thanks @wxiaoguang once I will get @lunny 's thought on this I will close this PR |

This pull request optimizes
CutDiffAroundLinein Gitea's git package (modules/git/diff.go) by replacing theregexp-based hunk header parsing with a custom fast byte-slice parser (parseHunkHeaderBytes) and replacing thebufio.Scanner-based string scanner with a zero-allocation byte scanning loop.The new implementation avoids heap allocations during the search and reduces CPU overhead when displaying diffs in comments and reviews, particularly for large files where target hunks are positioned early in the diff.
Verification
Verified with unit tests:
go test -count=1 ./modules/git/...Benchmarks:
go test -bench=. -benchmem ./modules/git/...Results
BenchmarkCutDiffAroundLineBenchmark Output