Skip to content

Commit 21ef907

Browse files
committed
Wrapping LSHost's cancellationtoken with a throttle
1 parent e62108c commit 21ef907

File tree

7 files changed

+80
-62
lines changed

7 files changed

+80
-62
lines changed

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ namespace ts.projectSystem {
176176
}
177177
};
178178

179-
export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller, projectServiceEventHandler?: server.ProjectServiceEventHandler, cancellationToken?: server.ServerCancellationToken) {
179+
export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller, projectServiceEventHandler?: server.ProjectServiceEventHandler, cancellationToken?: server.ServerCancellationToken, throttleWaitMilliseconds?: number) {
180180
if (typingsInstaller === undefined) {
181181
typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);
182182
}
183-
return new TestSession(host, cancellationToken || server.nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ projectServiceEventHandler !== undefined, projectServiceEventHandler);
183+
return new TestSession(host, cancellationToken || server.nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ projectServiceEventHandler !== undefined, projectServiceEventHandler, throttleWaitMilliseconds);
184184
}
185185

186186
export interface CreateProjectServiceParameters {
@@ -3320,6 +3320,7 @@ namespace ts.projectSystem {
33203320
},
33213321
resetRequest: noop
33223322
}
3323+
33233324
const session = createSession(host, /*typingsInstaller*/ undefined, /*projectServiceEventHandler*/ undefined, cancellationToken);
33243325

33253326
expectedRequestId = session.getNextSeq();
@@ -3492,7 +3493,7 @@ namespace ts.projectSystem {
34923493
};
34933494
const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3);
34943495
const host = createServerHost([f1, config]);
3495-
const session = createSession(host, /*typingsInstaller*/ undefined, () => { }, cancellationToken);
3496+
const session = createSession(host, /*typingsInstaller*/ undefined, () => { }, cancellationToken, /*throttleWaitMilliseconds*/ 0);
34963497
{
34973498
session.executeCommandSeq(<protocol.OpenRequest>{
34983499
command: "open",

src/server/editorServices.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@ namespace ts.server {
270270
public readonly cancellationToken: HostCancellationToken,
271271
public readonly useSingleInferredProject: boolean,
272272
readonly typingsInstaller: ITypingsInstaller = nullTypingsInstaller,
273-
private readonly eventHandler?: ProjectServiceEventHandler) {
273+
private readonly eventHandler?: ProjectServiceEventHandler,
274+
public readonly throttleWaitMilliseconds?: number) {
274275

275276
Debug.assert(!!host.createHash, "'ServerHost.createHash' is required for ProjectService");
276277

src/server/lsHost.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace ts.server {
1616
readonly realpath?: (path: string) => string;
1717

1818
constructor(private readonly host: ServerHost, private readonly project: Project, private readonly cancellationToken: HostCancellationToken) {
19+
this.cancellationToken = new ThrottledCancellationToken(cancellationToken, project.projectService.throttleWaitMilliseconds);
1920
this.getCanonicalFileName = ts.createGetCanonicalFileName(this.host.useCaseSensitiveFileNames);
2021

2122
if (host.trace) {

src/server/session.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,8 @@ namespace ts.server {
338338
private hrtime: (start?: number[]) => number[],
339339
protected logger: Logger,
340340
protected readonly canUseEvents: boolean,
341-
eventHandler?: ProjectServiceEventHandler) {
341+
eventHandler?: ProjectServiceEventHandler,
342+
private readonly throttleWaitMilliseconds?: number) {
342343

343344
this.eventHander = canUseEvents
344345
? eventHandler || (event => this.defaultEventHandler(event))
@@ -353,7 +354,7 @@ namespace ts.server {
353354
isCancellationRequested: () => cancellationToken.isCancellationRequested()
354355
}
355356
this.errorCheck = new MultistepOperation(multistepOperationHost);
356-
this.projectService = new ProjectService(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, this.eventHander);
357+
this.projectService = new ProjectService(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, this.eventHander, this.throttleWaitMilliseconds);
357358
this.gcTimer = new GcTimer(host, /*delay*/ 7000, logger);
358359
}
359360

src/services/navigationBar.ts

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@
22

33
/* @internal */
44
namespace ts.NavigationBar {
5+
/**
6+
* Matches all whitespace characters in a string. Eg:
7+
*
8+
* "app.
9+
*
10+
* onactivated"
11+
*
12+
* matches because of the newline, whereas
13+
*
14+
* "app.onactivated"
15+
*
16+
* does not match.
17+
*/
18+
const whiteSpaceRegex = /\s+/g;
19+
20+
// Keep sourceFile handy so we don't have to search for it every time we need to call `getText`.
21+
let curCancellationToken: CancellationToken;
22+
let curSourceFile: SourceFile;
23+
24+
/**
25+
* For performance, we keep navigation bar parents on a stack rather than passing them through each recursion.
26+
* `parent` is the current parent and is *not* stored in parentsStack.
27+
* `startNode` sets a new parent and `endNode` returns to the previous parent.
28+
*/
29+
let parentsStack: NavigationBarNode[] = [];
30+
let parent: NavigationBarNode;
31+
32+
// NavigationBarItem requires an array, but will not mutate it, so just give it this for performance.
33+
let emptyChildItemArray: NavigationBarItem[] = [];
34+
535
/**
636
* Represents a navigation bar item and its children.
737
* The returned NavigationBarItem is more complicated and doesn't include 'parent', so we use these to do work before converting.
@@ -21,8 +51,7 @@ namespace ts.NavigationBar {
2151
return map(topLevelItems(rootNavigationBarNode(sourceFile)), convertToTopLevelItem);
2252
}
2353
finally {
24-
curSourceFile = undefined;
25-
curCancellationToken = undefined;
54+
reset();
2655
}
2756
}
2857

@@ -33,14 +62,18 @@ namespace ts.NavigationBar {
3362
return convertToTree(rootNavigationBarNode(sourceFile));
3463
}
3564
finally {
36-
curSourceFile = undefined;
37-
curCancellationToken = undefined;
65+
reset();
3866
}
3967
}
4068

41-
// Keep sourceFile handy so we don't have to search for it every time we need to call `getText`.
42-
let curCancellationToken: CancellationToken;
43-
let curSourceFile: SourceFile;
69+
function reset() {
70+
curSourceFile = undefined;
71+
curCancellationToken = undefined;
72+
parentsStack = [];
73+
parent = undefined;
74+
emptyChildItemArray = [];
75+
}
76+
4477
function nodeText(node: Node): string {
4578
return node.getText(curSourceFile);
4679
}
@@ -58,14 +91,6 @@ namespace ts.NavigationBar {
5891
}
5992
}
6093

61-
/*
62-
For performance, we keep navigation bar parents on a stack rather than passing them through each recursion.
63-
`parent` is the current parent and is *not* stored in parentsStack.
64-
`startNode` sets a new parent and `endNode` returns to the previous parent.
65-
*/
66-
const parentsStack: NavigationBarNode[] = [];
67-
let parent: NavigationBarNode;
68-
6994
function rootNavigationBarNode(sourceFile: SourceFile): NavigationBarNode {
7095
Debug.assert(!parentsStack.length);
7196
const root: NavigationBarNode = { node: sourceFile, additionalNodes: undefined, parent: undefined, children: undefined, indent: 0 };
@@ -500,9 +525,6 @@ namespace ts.NavigationBar {
500525
}
501526
}
502527

503-
// NavigationBarItem requires an array, but will not mutate it, so just give it this for performance.
504-
const emptyChildItemArray: NavigationBarItem[] = [];
505-
506528
function convertToTree(n: NavigationBarNode): NavigationTree {
507529
return {
508530
text: getItemName(n.node),
@@ -623,19 +645,4 @@ namespace ts.NavigationBar {
623645
function isFunctionOrClassExpression(node: Node): boolean {
624646
return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction || node.kind === SyntaxKind.ClassExpression;
625647
}
626-
627-
/**
628-
* Matches all whitespace characters in a string. Eg:
629-
*
630-
* "app.
631-
*
632-
* onactivated"
633-
*
634-
* matches because of the newline, whereas
635-
*
636-
* "app.onactivated"
637-
*
638-
* does not match.
639-
*/
640-
const whiteSpaceRegex = /\s+/g;
641648
}

src/services/services.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,36 @@ namespace ts {
972972
}
973973
}
974974

975+
/* @internal */
976+
/** A cancellation that throttles calls to the host */
977+
export class ThrottledCancellationToken implements CancellationToken {
978+
// Store when we last tried to cancel. Checking cancellation can be expensive (as we have
979+
// to marshall over to the host layer). So we only bother actually checking once enough
980+
// time has passed.
981+
private lastCancellationCheckTime = 0;
982+
983+
constructor(private hostCancellationToken: HostCancellationToken, private readonly throttleWaitMilliseconds = 20) {
984+
}
985+
986+
public isCancellationRequested(): boolean {
987+
const time = timestamp();
988+
const duration = Math.abs(time - this.lastCancellationCheckTime);
989+
if (duration >= this.throttleWaitMilliseconds) {
990+
// Check no more than once every throttle wait milliseconds
991+
this.lastCancellationCheckTime = time;
992+
return this.hostCancellationToken.isCancellationRequested();
993+
}
994+
995+
return false;
996+
}
997+
998+
public throwIfCancellationRequested(): void {
999+
if (this.isCancellationRequested()) {
1000+
throw new OperationCanceledException();
1001+
}
1002+
}
1003+
}
1004+
9751005
export function createLanguageService(host: LanguageServiceHost,
9761006
documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory())): LanguageService {
9771007

src/services/shims.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -469,29 +469,6 @@ namespace ts {
469469
}
470470
}
471471

472-
/** A cancellation that throttles calls to the host */
473-
class ThrottledCancellationToken implements HostCancellationToken {
474-
// Store when we last tried to cancel. Checking cancellation can be expensive (as we have
475-
// to marshall over to the host layer). So we only bother actually checking once enough
476-
// time has passed.
477-
private lastCancellationCheckTime = 0;
478-
479-
constructor(private hostCancellationToken: HostCancellationToken) {
480-
}
481-
482-
public isCancellationRequested(): boolean {
483-
const time = timestamp();
484-
const duration = Math.abs(time - this.lastCancellationCheckTime);
485-
if (duration > 10) {
486-
// Check no more than once every 10 ms.
487-
this.lastCancellationCheckTime = time;
488-
return this.hostCancellationToken.isCancellationRequested();
489-
}
490-
491-
return false;
492-
}
493-
}
494-
495472
export class CoreServicesShimHostAdapter implements ParseConfigHost, ModuleResolutionHost {
496473

497474
public directoryExists: (directoryName: string) => boolean;

0 commit comments

Comments
 (0)