Skip to content

Collapse single-element blink edit loops into one splice - #192

Merged
TomasVotruba merged 1 commit into
mainfrom
blink-single-element-edits
Oct 9, 2026
Merged

TomasVotruba merged 1 commit into
mainfrom
blink-single-element-edits

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

Why

After #187 made same-length ReplaceRange an in-place slices.Replace, profiling BenchmarkFixPipeline/256classes showed memmove at ~47% of CPU. A chunk of that came from fixers that removed or inserted a contiguous run of tokens one element at a time, via RemoveAt/InsertAt in a loop. Each call shifts the whole tail once, so editing a k-token span costs k tail moves - O(n^2) on large files.

What

Replace those per-element loops with a single ReplaceRange (removal) or the new Stream.InsertSliceAt (insertion), so the tail shifts once per span instead of once per token. The set of edited tokens is identical, so output is byte-for-byte unchanged.

Touched: LongToShorthandOperator, StandardizeIncrement, IncrementStyle, AssignNullCoalescingToCoalesceEqual, EmptyLoopBody, convertLongArray, nullable-type removal, unused-import and alias removal, plus the csbInsert / nasReplace / modifier-reorder / operator-linebreak insert helpers.

Benchmarks

go test -bench=BenchmarkFixPipeline -benchmem:

size before after change
1class ~99.8us ~90.7us -9%
8classes ~636us ~600us -6%
64classes ~6.61ms ~6.12ms -7%
256classes ~49.3ms ~44.5ms -10%

No regression on the small-file path; the large cases improve.

Correctness

  • go test -race ./... passes; gofmt -l -s, go vet, modernize and golangci-lint are clean.
  • Each collapse edits the exact same tokens as the loop it replaces, so it is byte-identical by construction.
  • Verified empirically: blink output is byte-for-byte identical between this branch and main across all four parity corpora - rector-src (1663 files), mautic (4553), laravel (3083), rector (1279), 0 differing files.
  • vs-PHP parity harness stays green: mautic 0.07% (max 0.07%), rector-src 0.24% (max 0.25%).

Several fixers removed or inserted a contiguous run of tokens one element
at a time via RemoveAt/InsertAt in a loop. Each call shifted the whole
tail once, so a k-token span cost k tail moves, making those rules
O(n^2) on large files where memmove dominated the profile.

Replace the per-element loops with a single ReplaceRange or the new
InsertSliceAt, so the tail shifts once per span. The edited token set is
identical, so output is byte-for-byte unchanged.

BenchmarkFixPipeline:
  1class    ~99.8us -> ~90.7us  (-9%)
  8classes   ~636us -> ~600us   (-6%)
  64classes  ~6.61ms -> ~6.12ms (-7%)
  256classes ~49.3ms -> ~44.5ms (-10%)
@TomasVotruba
TomasVotruba merged commit 204260d into main Oct 9, 2026
21 checks passed
@TomasVotruba
TomasVotruba deleted the blink-single-element-edits branch October 9, 2026 21:18
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