Skip to content

Fix quadratic duplicate checks when merging comments - #673

Merged
KaanOzkan merged 4 commits into
mainfrom
fix-quadratic-merge-comment-checks
Oct 19, 2026
Merged

KaanOzkan merged 4 commits into
mainfrom
fix-quadratic-merge-comment-checks

Conversation

@jesse-shopify

Copy link
Copy Markdown
Contributor

Problem

NodeWithComments#merge_with appends the other node's comments one at a time and calls Array#include? on the growing comments array for each of them. Merging a tree with N distinct comments therefore performs about N²/2 comment comparisons. This is reached for file header comments, which are merged into the MergeTree for every root tree, and for comments on definitions that exist in both trees.

This was reported through an internal security review as a CPU-exhaustion vector for any consumer that merges untrusted RBI text. Reproduction with 20,000 distinct comments:

Scenario Before After
distinct file header comments 10.1s 0.005s
same class carrying the comments in both trees 31.9s 0.018s

Before the fix, doubling the comment count roughly quadrupled merge time.

Fix

Index the existing comments by text and only compare a merged comment with the indexed comments sharing its text. The comparison still goes through Comment#==, so the comments that survive a merge and their order are unchanged. In particular the existing asymmetry between plain and RBS comments is preserved as is.

A differential fuzz of 50,000 random mixes of Comment, RBSComment and BlankLine objects, comparing the old include? loop with the new code, produced identical results in every case.

Tests

test_merge_many_distinct_comments_does_not_compare_every_pair merges a tree with 2,000 distinct header comments into one with the same comments plus one more, asserts the merged comments and their order, and counts == calls on the comment objects. It failed before the fix with 4,002,000 checks against a bound of 6,000.

Not in this PR

The Attr and Method overrides in the same file dedupe sigs with the same include? pattern, so a definition with many distinct sigs on one side is also quadratic. Sig has no hash key consistent with its ==, so that is a larger change and is left for a follow-up.

🤖 Generated with Claude Code

`NodeWithComments#merge_with` appended the other node's comments one at a
time and called `Array#include?` on the growing `comments` array for each
of them. Merging a tree with N distinct comments therefore performed about
N²/2 comment comparisons. This is reached for file header comments, which
are merged into the `MergeTree` for every root tree, and for comments on
definitions that exist in both trees.

Index the existing comments by text and only compare a merged comment
with the indexed comments sharing its text. The check still goes through
`Comment#==`, so the comments that survive a merge and their order are
unchanged. A differential fuzz of 50,000 random mixes of plain, RBS and
blank comments produced identical results for the old and new code.

Merging 20,000 distinct header comments goes from 10s to 5ms, and merging
a class carrying 20,000 comments in both trees goes from 32s to 18ms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jesse-shopify
jesse-shopify requested a review from a team as a code owner October 16, 2026 23:47
jesse-shopify and others added 2 commits October 16, 2026 16:48
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
res = tree1.merge(tree2)

assert_equal(["New comment"], res.comments.map(&:text))
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added new test

@KaanOzkan
KaanOzkan merged commit 31b87a8 into main Oct 19, 2026
11 checks passed
@KaanOzkan
KaanOzkan deleted the fix-quadratic-merge-comment-checks branch October 19, 2026 13:27
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.

3 participants