Skip to content

Commit 2f4c4d0

Browse files
committed
Reland "[clangd] Mechanism to make update debounce responsive to rebuild speed."
This reverts commit ed98994. Removed the accidental double-mutex-unlock.
1 parent 3ed1223 commit 2f4c4d0

File tree

5 files changed

+108
-14
lines changed

5 files changed

+108
-14
lines changed

clang-tools-extra/clangd/ClangdServer.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ struct UpdateIndexCallbacks : public ParsingCallbacks {
106106

107107
ClangdServer::Options ClangdServer::optsForTest() {
108108
ClangdServer::Options Opts;
109-
Opts.UpdateDebounce = std::chrono::steady_clock::duration::zero(); // Faster!
109+
Opts.UpdateDebounce = DebouncePolicy::fixed(/*zero*/ {});
110110
Opts.StorePreamblesInMemory = true;
111111
Opts.AsyncThreadsCount = 4; // Consistent!
112112
Opts.SemanticHighlighting = true;

clang-tools-extra/clangd/ClangdServer.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ class ClangdServer {
130130
llvm::Optional<std::string> ResourceDir = llvm::None;
131131

132132
/// Time to wait after a new file version before computing diagnostics.
133-
std::chrono::steady_clock::duration UpdateDebounce =
134-
std::chrono::milliseconds(500);
133+
DebouncePolicy UpdateDebounce =
134+
DebouncePolicy::fixed(std::chrono::milliseconds(500));
135135

136136
bool SuggestMissingIncludes = false;
137137

clang-tools-extra/clangd/TUScheduler.cpp

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
#include "llvm/Support/Threading.h"
6262
#include <algorithm>
6363
#include <memory>
64+
#include <mutex>
6465
#include <queue>
6566
#include <thread>
6667

@@ -164,7 +165,7 @@ class ASTWorker {
164165
friend class ASTWorkerHandle;
165166
ASTWorker(PathRef FileName, const GlobalCompilationDatabase &CDB,
166167
TUScheduler::ASTCache &LRUCache, Semaphore &Barrier, bool RunSync,
167-
steady_clock::duration UpdateDebounce, bool StorePreamblesInMemory,
168+
DebouncePolicy UpdateDebounce, bool StorePreamblesInMemory,
168169
ParsingCallbacks &Callbacks);
169170

170171
public:
@@ -176,7 +177,7 @@ class ASTWorker {
176177
static ASTWorkerHandle
177178
create(PathRef FileName, const GlobalCompilationDatabase &CDB,
178179
TUScheduler::ASTCache &IdleASTs, AsyncTaskRunner *Tasks,
179-
Semaphore &Barrier, steady_clock::duration UpdateDebounce,
180+
Semaphore &Barrier, DebouncePolicy UpdateDebounce,
180181
bool StorePreamblesInMemory, ParsingCallbacks &Callbacks);
181182
~ASTWorker();
182183

@@ -242,7 +243,7 @@ class ASTWorker {
242243
TUScheduler::ASTCache &IdleASTs;
243244
const bool RunSync;
244245
/// Time to wait after an update to see whether another update obsoletes it.
245-
const steady_clock::duration UpdateDebounce;
246+
const DebouncePolicy UpdateDebounce;
246247
/// File that ASTWorker is responsible for.
247248
const Path FileName;
248249
const GlobalCompilationDatabase &CDB;
@@ -263,6 +264,9 @@ class ASTWorker {
263264
/// be consumed by clients of ASTWorker.
264265
std::shared_ptr<const ParseInputs> FileInputs; /* GUARDED_BY(Mutex) */
265266
std::shared_ptr<const PreambleData> LastBuiltPreamble; /* GUARDED_BY(Mutex) */
267+
/// Times of recent AST rebuilds, used for UpdateDebounce computation.
268+
llvm::SmallVector<DebouncePolicy::clock::duration, 8>
269+
RebuildTimes; /* GUARDED_BY(Mutex) */
266270
/// Becomes ready when the first preamble build finishes.
267271
Notification PreambleWasBuilt;
268272
/// Set to true to signal run() to finish processing.
@@ -326,7 +330,7 @@ class ASTWorkerHandle {
326330
ASTWorkerHandle
327331
ASTWorker::create(PathRef FileName, const GlobalCompilationDatabase &CDB,
328332
TUScheduler::ASTCache &IdleASTs, AsyncTaskRunner *Tasks,
329-
Semaphore &Barrier, steady_clock::duration UpdateDebounce,
333+
Semaphore &Barrier, DebouncePolicy UpdateDebounce,
330334
bool StorePreamblesInMemory, ParsingCallbacks &Callbacks) {
331335
std::shared_ptr<ASTWorker> Worker(
332336
new ASTWorker(FileName, CDB, IdleASTs, Barrier, /*RunSync=*/!Tasks,
@@ -340,7 +344,7 @@ ASTWorker::create(PathRef FileName, const GlobalCompilationDatabase &CDB,
340344

341345
ASTWorker::ASTWorker(PathRef FileName, const GlobalCompilationDatabase &CDB,
342346
TUScheduler::ASTCache &LRUCache, Semaphore &Barrier,
343-
bool RunSync, steady_clock::duration UpdateDebounce,
347+
bool RunSync, DebouncePolicy UpdateDebounce,
344348
bool StorePreamblesInMemory, ParsingCallbacks &Callbacks)
345349
: IdleASTs(LRUCache), RunSync(RunSync), UpdateDebounce(UpdateDebounce),
346350
FileName(FileName), CDB(CDB),
@@ -488,6 +492,7 @@ void ASTWorker::update(ParseInputs Inputs, WantDiagnostics WantDiags) {
488492

489493
// Get the AST for diagnostics.
490494
llvm::Optional<std::unique_ptr<ParsedAST>> AST = IdleASTs.take(this);
495+
auto RebuildStartTime = DebouncePolicy::clock::now();
491496
if (!AST) {
492497
llvm::Optional<ParsedAST> NewAST =
493498
buildAST(FileName, std::move(Invocation), CompilerInvocationDiags,
@@ -510,6 +515,18 @@ void ASTWorker::update(ParseInputs Inputs, WantDiagnostics WantDiags) {
510515
// spam us with updates.
511516
// Note *AST can still be null if buildAST fails.
512517
if (*AST) {
518+
{
519+
// Try to record the AST-build time, to inform future update debouncing.
520+
// This is best-effort only: if the lock is held, don't bother.
521+
auto RebuildDuration = DebouncePolicy::clock::now() - RebuildStartTime;
522+
std::unique_lock<std::mutex> Lock(Mutex, std::try_to_lock);
523+
if (Lock.owns_lock()) {
524+
// Do not let RebuildTimes grow beyond its small-size (i.e. capacity).
525+
if (RebuildTimes.size() == RebuildTimes.capacity())
526+
RebuildTimes.erase(RebuildTimes.begin());
527+
RebuildTimes.push_back(RebuildDuration);
528+
}
529+
}
513530
trace::Span Span("Running main AST callback");
514531

515532
Callbacks.onMainAST(FileName, **AST, RunPublish);
@@ -750,13 +767,13 @@ Deadline ASTWorker::scheduleLocked() {
750767
assert(!Requests.empty() && "skipped the whole queue");
751768
// Some updates aren't dead yet, but never end up being used.
752769
// e.g. the first keystroke is live until obsoleted by the second.
753-
// We debounce "maybe-unused" writes, sleeping 500ms in case they become dead.
770+
// We debounce "maybe-unused" writes, sleeping in case they become dead.
754771
// But don't delay reads (including updates where diagnostics are needed).
755772
for (const auto &R : Requests)
756773
if (R.UpdateType == None || R.UpdateType == WantDiagnostics::Yes)
757774
return Deadline::zero();
758775
// Front request needs to be debounced, so determine when we're ready.
759-
Deadline D(Requests.front().AddTime + UpdateDebounce);
776+
Deadline D(Requests.front().AddTime + UpdateDebounce.compute(RebuildTimes));
760777
return D;
761778
}
762779

@@ -1036,5 +1053,33 @@ std::vector<Path> TUScheduler::getFilesWithCachedAST() const {
10361053
return Result;
10371054
}
10381055

1056+
DebouncePolicy::clock::duration
1057+
DebouncePolicy::compute(llvm::ArrayRef<clock::duration> History) const {
1058+
assert(Min <= Max && "Invalid policy");
1059+
if (History.empty())
1060+
return Max; // Arbitrary.
1061+
1062+
// Base the result on the median rebuild.
1063+
// nth_element needs a mutable array, take the chance to bound the data size.
1064+
History = History.take_back(15);
1065+
llvm::SmallVector<clock::duration, 15> Recent(History.begin(), History.end());
1066+
auto Median = Recent.begin() + Recent.size() / 2;
1067+
std::nth_element(Recent.begin(), Median, Recent.end());
1068+
1069+
clock::duration Target =
1070+
std::chrono::duration_cast<clock::duration>(RebuildRatio * *Median);
1071+
if (Target > Max)
1072+
return Max;
1073+
if (Target < Min)
1074+
return Min;
1075+
return Target;
1076+
}
1077+
1078+
DebouncePolicy DebouncePolicy::fixed(clock::duration T) {
1079+
DebouncePolicy P;
1080+
P.Min = P.Max = T;
1081+
return P;
1082+
}
1083+
10391084
} // namespace clangd
10401085
} // namespace clang

clang-tools-extra/clangd/TUScheduler.h

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@ struct ASTRetentionPolicy {
6161
unsigned MaxRetainedASTs = 3;
6262
};
6363

64+
/// Clangd may wait after an update to see if another one comes along.
65+
/// This is so we rebuild once the user stops typing, not when they start.
66+
/// Debounce may be disabled/interrupted if we must build this version.
67+
/// The debounce time is responsive to user preferences and rebuild time.
68+
/// In the future, we could also consider different types of edits.
69+
struct DebouncePolicy {
70+
using clock = std::chrono::steady_clock;
71+
72+
/// The minimum time that we always debounce for.
73+
clock::duration Min = /*zero*/ {};
74+
/// The maximum time we may debounce for.
75+
clock::duration Max = /*zero*/ {};
76+
/// Target debounce, as a fraction of file rebuild time.
77+
/// e.g. RebuildRatio = 2, recent builds took 200ms => debounce for 400ms.
78+
float RebuildRatio = 1;
79+
80+
/// Compute the time to debounce based on this policy and recent build times.
81+
clock::duration compute(llvm::ArrayRef<clock::duration> History) const;
82+
/// A policy that always returns the same duration, useful for tests.
83+
static DebouncePolicy fixed(clock::duration);
84+
};
85+
6486
struct TUAction {
6587
enum State {
6688
Queued, // The TU is pending in the thread task queue to be built.
@@ -158,7 +180,7 @@ class TUScheduler {
158180

159181
/// Time to wait after an update to see if another one comes along.
160182
/// This tries to ensure we rebuild once the user stops typing.
161-
std::chrono::steady_clock::duration UpdateDebounce = /*zero*/ {};
183+
DebouncePolicy UpdateDebounce;
162184

163185
/// Determines when to keep idle ASTs in memory for future use.
164186
ASTRetentionPolicy RetentionPolicy;
@@ -273,7 +295,7 @@ class TUScheduler {
273295
// asynchronously.
274296
llvm::Optional<AsyncTaskRunner> PreambleTasks;
275297
llvm::Optional<AsyncTaskRunner> WorkerThreads;
276-
std::chrono::steady_clock::duration UpdateDebounce;
298+
DebouncePolicy UpdateDebounce;
277299
};
278300

279301
} // namespace clangd

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "gmock/gmock.h"
2424
#include "gtest/gtest.h"
2525
#include <algorithm>
26+
#include <chrono>
2627
#include <utility>
2728

2829
namespace clang {
@@ -208,7 +209,7 @@ TEST_F(TUSchedulerTests, Debounce) {
208209
std::atomic<int> CallbackCount(0);
209210
{
210211
auto Opts = optsForTest();
211-
Opts.UpdateDebounce = std::chrono::seconds(1);
212+
Opts.UpdateDebounce = DebouncePolicy::fixed(std::chrono::seconds(1));
212213
TUScheduler S(CDB, Opts, captureDiags());
213214
// FIXME: we could probably use timeouts lower than 1 second here.
214215
auto Path = testPath("foo.cpp");
@@ -361,7 +362,7 @@ TEST_F(TUSchedulerTests, ManyUpdates) {
361362
// Run TUScheduler and collect some stats.
362363
{
363364
auto Opts = optsForTest();
364-
Opts.UpdateDebounce = std::chrono::milliseconds(50);
365+
Opts.UpdateDebounce = DebouncePolicy::fixed(std::chrono::milliseconds(50));
365366
TUScheduler S(CDB, Opts, captureDiags());
366367

367368
std::vector<std::string> Files;
@@ -754,6 +755,32 @@ TEST_F(TUSchedulerTests, CommandLineWarnings) {
754755
EXPECT_THAT(Diagnostics, IsEmpty());
755756
}
756757

758+
TEST(DebouncePolicy, Compute) {
759+
namespace c = std::chrono;
760+
std::vector<DebouncePolicy::clock::duration> History = {
761+
c::seconds(0),
762+
c::seconds(5),
763+
c::seconds(10),
764+
c::seconds(20),
765+
};
766+
DebouncePolicy Policy;
767+
Policy.Min = c::seconds(3);
768+
Policy.Max = c::seconds(25);
769+
// Call Policy.compute(History) and return seconds as a float.
770+
auto Compute = [&](llvm::ArrayRef<DebouncePolicy::clock::duration> History) {
771+
using FloatingSeconds = c::duration<float, c::seconds::period>;
772+
return static_cast<float>(Policy.compute(History) / FloatingSeconds(1));
773+
};
774+
EXPECT_NEAR(10, Compute(History), 0.01) << "(upper) median = 10";
775+
Policy.RebuildRatio = 1.5;
776+
EXPECT_NEAR(15, Compute(History), 0.01) << "median = 10, ratio = 1.5";
777+
Policy.RebuildRatio = 3;
778+
EXPECT_NEAR(25, Compute(History), 0.01) << "constrained by max";
779+
Policy.RebuildRatio = 0;
780+
EXPECT_NEAR(3, Compute(History), 0.01) << "constrained by min";
781+
EXPECT_NEAR(25, Compute({}), 0.01) << "no history -> max";
782+
}
783+
757784
} // namespace
758785
} // namespace clangd
759786
} // namespace clang

0 commit comments

Comments
 (0)