Skip to content

perf(source): build migrations index lazily to avoid quadratic startup - #1424

Open
YuheiTakagawa wants to merge 1 commit into
golang-migrate:masterfrom
YuheiTakagawa:perf/source-lazy-index-build
Open

perf(source): build migrations index lazily to avoid quadratic startup#1424
YuheiTakagawa wants to merge 1 commit into
golang-migrate:masterfrom
YuheiTakagawa:perf/source-lazy-index-build

Conversation

@YuheiTakagawa

@YuheiTakagawa YuheiTakagawa commented Aug 9, 2026

Copy link
Copy Markdown

Problem

source.Migrations.Append calls buildIndex() on every append, which rebuilds and re-sorts the entire version index each time. Since every in-memory source driver (file, iofs, aws_s3, github, bitbucket, go_bindata, ...) calls Append in a loop when the driver is opened/initialized, loading n migration files costs O(n² log n).

For long-lived projects this makes startup noticeably slow before any migration is even executed. Large migration counts are a real use case (see e.g. #1412, which fixes S3 listing for >1000 migrations).

Fix

Append now only marks the index as dirty. The index is rebuilt at most once, lazily, on the first read access (First, or Prev/Next via findPos). This keeps the total cost at O(n log n) regardless of how many files are appended.

  • No exported API changes; Append's duplicate-rejection behavior is unchanged.
  • No change to the (already non-)thread-safety contract of Migrations: drivers build it once at init and read afterwards, same as before.

Benchmarks

Added BenchmarkAppend{100,1000,5000} (append n up/down pairs, then one read so the index build is included). Same benchmark code against both implementations (i9-9900K, Windows, go1.25; the n=10,000 row was measured with an equivalent out-of-tree benchmark):

n (versions) before after
100 0.69 ms/op 0.05 ms/op
1,000 70.4 ms/op 0.41 ms/op
5,000 2.12 s/op 2.0 ms/op
10,000 11.3 s/op 7.0 ms/op

Before scales ~quadratically (4x files → ~16x time); after is linearithmic.

Verification

  • go test ./source/ — passes (existing tests + new interleaved append/read regression test)
  • go vet ./source/ — clean
  • golangci-lint run ./source/ — no new issues

Migrations.Append rebuilt and re-sorted the whole index on every call,
making source driver initialization O(n^2 log n) in the number of
migration files. All in-memory source drivers (file, iofs, aws_s3,
github, ...) call Append in a loop at Open/Init time, so startup cost
grew quadratically: 81ms at 1,000 migrations, 2.2s at 5,000 and 11.3s
at 10,000 on an i9-9900K.

Append now only marks the index dirty; the index is rebuilt at most
once, on the first read access (First/Prev/Next). No exported API
changes.

BenchmarkAppend5000 drops from ~2.2s/op to ~2ms/op.
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 54.911% (+0.5%) from 54.412% — YuheiTakagawa:perf/source-lazy-index-build into golang-migrate:master

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.

2 participants