Skip to content

Commit 07f9fb8

Browse files
committed
[clangd] Elide even more checks in SelectionTree.
During pop() we convert nodes into spans of expanded syntax::Tokens. If we precompute a range of plausible (expanded) tokens, then we can do an extremely cheap approximate hit-test against it, because syntax::Tokens are ordered by pointer. This would seem not to buy anything (we don't enter nodes unless they overlap the selection), but in fact the spans we have are for *newly* claimed ranges (i.e. those unclaimed by any child node). So if you have: { { [[2+2]]; } } then all of the CompoundStmts pass the hit test and are pushed, but we skip full hit-testing of the brackets during pop() as they lie outside the range. This is ~10x average speedup for selectiontree on a bad case I've seen (large gtest file). Differential Revision: https://reviews.llvm.org/D117107
1 parent 004acbb commit 07f9fb8

File tree

2 files changed

+102
-18
lines changed

2 files changed

+102
-18
lines changed

clang-tools-extra/clangd/Selection.cpp

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -274,22 +274,37 @@ class SelectionTester {
274274
for (unsigned I = 0; I < Sel.size(); ++I) {
275275
if (shouldIgnore(Sel[I]) || PPIgnored[I])
276276
continue;
277-
SpelledTokens.emplace_back();
278-
Tok &S = SpelledTokens.back();
277+
SelectedSpelled.emplace_back();
278+
Tok &S = SelectedSpelled.back();
279279
S.Offset = SM.getFileOffset(Sel[I].location());
280280
if (S.Offset >= SelBegin && S.Offset + Sel[I].length() <= SelEnd)
281281
S.Selected = SelectionTree::Complete;
282282
else
283283
S.Selected = SelectionTree::Partial;
284284
}
285+
MaybeSelectedExpanded = computeMaybeSelectedExpandedTokens(Buf);
285286
}
286287

287288
// Test whether a consecutive range of tokens is selected.
288289
// The tokens are taken from the expanded token stream.
289290
SelectionTree::Selection
290291
test(llvm::ArrayRef<syntax::Token> ExpandedTokens) const {
291-
if (SpelledTokens.empty())
292+
if (ExpandedTokens.empty())
292293
return NoTokens;
294+
if (SelectedSpelled.empty())
295+
return SelectionTree::Unselected;
296+
// Cheap (pointer) check whether any of the tokens could touch selection.
297+
// In most cases, the node's overall source range touches ExpandedTokens,
298+
// or we would have failed mayHit(). However now we're only considering
299+
// the *unclaimed* spans of expanded tokens.
300+
// This is a significant performance improvement when a lot of nodes
301+
// surround the selection, including when generated by macros.
302+
if (MaybeSelectedExpanded.empty() ||
303+
&ExpandedTokens.front() > &MaybeSelectedExpanded.back() ||
304+
&ExpandedTokens.back() < &MaybeSelectedExpanded.front()) {
305+
return SelectionTree::Unselected;
306+
}
307+
293308
SelectionTree::Selection Result = NoTokens;
294309
while (!ExpandedTokens.empty()) {
295310
// Take consecutive tokens from the same context together for efficiency.
@@ -312,14 +327,14 @@ class SelectionTester {
312327
// If it returns false, test() will return NoTokens or Unselected.
313328
// If it returns true, test() may return any value.
314329
bool mayHit(SourceRange R) const {
315-
if (SpelledTokens.empty())
330+
if (SelectedSpelled.empty() || MaybeSelectedExpanded.empty())
316331
return false;
317332
// If the node starts after the selection ends, it is not selected.
318333
// Tokens a macro location might claim are >= its expansion start.
319334
// So if the expansion start > last selected token, we can prune it.
320335
// (This is particularly helpful for GTest's TEST macro).
321336
if (auto B = offsetInSelFile(getExpansionStart(R.getBegin())))
322-
if (*B > SpelledTokens.back().Offset)
337+
if (*B > SelectedSpelled.back().Offset)
323338
return false;
324339
// If the node ends before the selection begins, it is not selected.
325340
SourceLocation EndLoc = R.getEnd();
@@ -328,12 +343,72 @@ class SelectionTester {
328343
// In the rare case that the expansion range is a char range, EndLoc is
329344
// ~one token too far to the right. We may fail to prune, that's OK.
330345
if (auto E = offsetInSelFile(EndLoc))
331-
if (*E < SpelledTokens.front().Offset)
346+
if (*E < SelectedSpelled.front().Offset)
332347
return false;
333348
return true;
334349
}
335350

336351
private:
352+
// Plausible expanded tokens that might be affected by the selection.
353+
// This is an overestimate, it may contain tokens that are not selected.
354+
// The point is to allow cheap pruning in test()
355+
llvm::ArrayRef<syntax::Token>
356+
computeMaybeSelectedExpandedTokens(const syntax::TokenBuffer &Toks) {
357+
if (SelectedSpelled.empty())
358+
return {};
359+
360+
bool StartInvalid = false;
361+
const syntax::Token *Start = llvm::partition_point(
362+
Toks.expandedTokens(),
363+
[&, First = SelectedSpelled.front().Offset](const syntax::Token &Tok) {
364+
if (Tok.kind() == tok::eof)
365+
return false;
366+
// Implausible if upperbound(Tok) < First.
367+
SourceLocation Loc = Tok.location();
368+
auto Offset = offsetInSelFile(Loc);
369+
while (Loc.isValid() && !Offset) {
370+
Loc = Loc.isMacroID() ? SM.getImmediateExpansionRange(Loc).getEnd()
371+
: SM.getIncludeLoc(SM.getFileID(Loc));
372+
Offset = offsetInSelFile(Loc);
373+
}
374+
if (Offset)
375+
return *Offset < First;
376+
StartInvalid = true;
377+
return false; // conservatively assume this token can overlap
378+
});
379+
if (StartInvalid) {
380+
assert(false && "Expanded tokens could not be resolved to main file!");
381+
Start = Toks.expandedTokens().begin();
382+
}
383+
384+
bool EndInvalid = false;
385+
const syntax::Token *End = llvm::partition_point(
386+
Toks.expandedTokens(),
387+
[&, Last = SelectedSpelled.back().Offset](const syntax::Token &Tok) {
388+
if (Tok.kind() == tok::eof)
389+
return false;
390+
// Plausible if lowerbound(Tok) <= Last.
391+
SourceLocation Loc = Tok.location();
392+
auto Offset = offsetInSelFile(Loc);
393+
while (Loc.isValid() && !Offset) {
394+
Loc = Loc.isMacroID()
395+
? SM.getImmediateExpansionRange(Loc).getBegin()
396+
: SM.getIncludeLoc(SM.getFileID(Loc));
397+
Offset = offsetInSelFile(Loc);
398+
}
399+
if (Offset)
400+
return *Offset <= Last;
401+
EndInvalid = true;
402+
return true; // conservatively assume this token can overlap
403+
});
404+
if (EndInvalid) {
405+
assert(false && "Expanded tokens could not be resolved to main file!");
406+
End = Toks.expandedTokens().end();
407+
}
408+
409+
return llvm::makeArrayRef(Start, End);
410+
}
411+
337412
// Hit-test a consecutive range of tokens from a single file ID.
338413
SelectionTree::Selection
339414
testChunk(FileID FID, llvm::ArrayRef<syntax::Token> Batch) const {
@@ -389,19 +464,20 @@ class SelectionTester {
389464
SelectionTree::Selection testTokenRange(unsigned Begin, unsigned End) const {
390465
assert(Begin <= End);
391466
// Outside the selection entirely?
392-
if (End < SpelledTokens.front().Offset ||
393-
Begin > SpelledTokens.back().Offset)
467+
if (End < SelectedSpelled.front().Offset ||
468+
Begin > SelectedSpelled.back().Offset)
394469
return SelectionTree::Unselected;
395470

396471
// Compute range of tokens.
397472
auto B = llvm::partition_point(
398-
SpelledTokens, [&](const Tok &T) { return T.Offset < Begin; });
399-
auto E = std::partition_point(
400-
B, SpelledTokens.end(), [&](const Tok &T) { return T.Offset <= End; });
473+
SelectedSpelled, [&](const Tok &T) { return T.Offset < Begin; });
474+
auto E = std::partition_point(B, SelectedSpelled.end(), [&](const Tok &T) {
475+
return T.Offset <= End;
476+
});
401477

402478
// Aggregate selectedness of tokens in range.
403-
bool ExtendsOutsideSelection = Begin < SpelledTokens.front().Offset ||
404-
End > SpelledTokens.back().Offset;
479+
bool ExtendsOutsideSelection = Begin < SelectedSpelled.front().Offset ||
480+
End > SelectedSpelled.back().Offset;
405481
SelectionTree::Selection Result =
406482
ExtendsOutsideSelection ? SelectionTree::Unselected : NoTokens;
407483
for (auto It = B; It != E; ++It)
@@ -412,13 +488,13 @@ class SelectionTester {
412488
// Is the token at `Offset` selected?
413489
SelectionTree::Selection testToken(unsigned Offset) const {
414490
// Outside the selection entirely?
415-
if (Offset < SpelledTokens.front().Offset ||
416-
Offset > SpelledTokens.back().Offset)
491+
if (Offset < SelectedSpelled.front().Offset ||
492+
Offset > SelectedSpelled.back().Offset)
417493
return SelectionTree::Unselected;
418494
// Find the token, if it exists.
419495
auto It = llvm::partition_point(
420-
SpelledTokens, [&](const Tok &T) { return T.Offset < Offset; });
421-
if (It != SpelledTokens.end() && It->Offset == Offset)
496+
SelectedSpelled, [&](const Tok &T) { return T.Offset < Offset; });
497+
if (It != SelectedSpelled.end() && It->Offset == Offset)
422498
return It->Selected;
423499
return NoTokens;
424500
}
@@ -444,7 +520,8 @@ class SelectionTester {
444520
unsigned Offset;
445521
SelectionTree::Selection Selected;
446522
};
447-
std::vector<Tok> SpelledTokens;
523+
std::vector<Tok> SelectedSpelled;
524+
llvm::ArrayRef<syntax::Token> MaybeSelectedExpanded;
448525
FileID SelFile;
449526
SourceRange SelFileBounds;
450527
const SourceManager &SM;

clang-tools-extra/clangd/unittests/SelectionTests.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ TEST(SelectionTest, CommonAncestor) {
201201
)cpp",
202202
nullptr,
203203
},
204+
{
205+
R"cpp(
206+
#define TARGET void foo()
207+
[[TAR^GET{ return; }]]
208+
)cpp",
209+
"FunctionDecl",
210+
},
204211
{
205212
R"cpp(
206213
struct S { S(const char*); };

0 commit comments

Comments
 (0)