Skip to content

Commit 25f0750

Browse files
authored
Merge pull request #14214 from Microsoft/CancellationChecksForLowPriorityTasks
Adding cancellation token checks for lower priority tasks (navbar & outlining spans)
2 parents 5e4b8d6 + d20cebf commit 25f0750

File tree

8 files changed

+211
-88
lines changed

8 files changed

+211
-88
lines changed

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 114 additions & 23 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 {
@@ -547,6 +547,49 @@ namespace ts.projectSystem {
547547
readonly getEnvironmentVariable = notImplemented;
548548
}
549549

550+
/**
551+
* Test server cancellation token used to mock host token cancellation requests.
552+
* The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls
553+
* should be made before canceling the token. The id of the request to cancel should be set with
554+
* setRequestToCancel();
555+
*/
556+
export class TestServerCancellationToken implements server.ServerCancellationToken {
557+
private currentId = -1;
558+
private requestToCancel = -1;
559+
private isCancellationRequestedCount = 0;
560+
561+
constructor(private cancelAfterRequest = 0) {
562+
}
563+
564+
setRequest(requestId: number) {
565+
this.currentId = requestId;
566+
}
567+
568+
setRequestToCancel(requestId: number) {
569+
this.resetToken();
570+
this.requestToCancel = requestId;
571+
}
572+
573+
resetRequest(requestId: number) {
574+
assert.equal(requestId, this.currentId, "unexpected request id in cancellation");
575+
this.currentId = undefined;
576+
}
577+
578+
isCancellationRequested() {
579+
this.isCancellationRequestedCount++;
580+
// If the request id is the request to cancel and isCancellationRequestedCount
581+
// has been met then cancel the request. Ex: cancel the request if it is a
582+
// nav bar request & isCancellationRequested() has already been called three times.
583+
return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest;
584+
}
585+
586+
resetToken() {
587+
this.currentId = -1;
588+
this.isCancellationRequestedCount = 0;
589+
this.requestToCancel = -1;
590+
}
591+
}
592+
550593
export function makeSessionRequest<T>(command: string, args: T) {
551594
const newRequest: protocol.Request = {
552595
seq: 0,
@@ -3384,6 +3427,7 @@ namespace ts.projectSystem {
33843427
},
33853428
resetRequest: noop
33863429
};
3430+
33873431
const session = createSession(host, /*typingsInstaller*/ undefined, /*projectServiceEventHandler*/ undefined, cancellationToken);
33883432

33893433
expectedRequestId = session.getNextSeq();
@@ -3422,22 +3466,7 @@ namespace ts.projectSystem {
34223466
})
34233467
};
34243468

3425-
let requestToCancel = -1;
3426-
const cancellationToken: server.ServerCancellationToken = (function(){
3427-
let currentId: number;
3428-
return <server.ServerCancellationToken>{
3429-
setRequest(requestId) {
3430-
currentId = requestId;
3431-
},
3432-
resetRequest(requestId) {
3433-
assert.equal(requestId, currentId, "unexpected request id in cancellation");
3434-
currentId = undefined;
3435-
},
3436-
isCancellationRequested() {
3437-
return requestToCancel === currentId;
3438-
}
3439-
};
3440-
})();
3469+
const cancellationToken = new TestServerCancellationToken();
34413470
const host = createServerHost([f1, config]);
34423471
const session = createSession(host, /*typingsInstaller*/ undefined, () => {}, cancellationToken);
34433472
{
@@ -3472,13 +3501,13 @@ namespace ts.projectSystem {
34723501
host.clearOutput();
34733502

34743503
// cancel previously issued Geterr
3475-
requestToCancel = getErrId;
3504+
cancellationToken.setRequestToCancel(getErrId);
34763505
host.runQueuedTimeoutCallbacks();
34773506

34783507
assert.equal(host.getOutput().length, 1, "expect 1 message");
34793508
verifyRequestCompleted(getErrId, 0);
34803509

3481-
requestToCancel = -1;
3510+
cancellationToken.resetToken();
34823511
}
34833512
{
34843513
const getErrId = session.getNextSeq();
@@ -3495,12 +3524,12 @@ namespace ts.projectSystem {
34953524
assert.equal(e1.event, "syntaxDiag");
34963525
host.clearOutput();
34973526

3498-
requestToCancel = getErrId;
3527+
cancellationToken.setRequestToCancel(getErrId);
34993528
host.runQueuedImmediateCallbacks();
35003529
assert.equal(host.getOutput().length, 1, "expect 1 message");
35013530
verifyRequestCompleted(getErrId, 0);
35023531

3503-
requestToCancel = -1;
3532+
cancellationToken.resetToken();
35043533
}
35053534
{
35063535
const getErrId = session.getNextSeq();
@@ -3523,7 +3552,7 @@ namespace ts.projectSystem {
35233552
assert.equal(e2.event, "semanticDiag");
35243553
verifyRequestCompleted(getErrId, 1);
35253554

3526-
requestToCancel = -1;
3555+
cancellationToken.resetToken();
35273556
}
35283557
{
35293558
const getErr1 = session.getNextSeq();
@@ -3558,6 +3587,68 @@ namespace ts.projectSystem {
35583587
return JSON.parse(server.extractMessage(host.getOutput()[n]));
35593588
}
35603589
});
3590+
it("Lower priority tasks are cancellable", () => {
3591+
const f1 = {
3592+
path: "/a/app.ts",
3593+
content: `{ let x = 1; } var foo = "foo"; var bar = "bar"; var fooBar = "fooBar";`
3594+
};
3595+
const config = {
3596+
path: "/a/tsconfig.json",
3597+
content: JSON.stringify({
3598+
compilerOptions: {}
3599+
})
3600+
};
3601+
const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3);
3602+
const host = createServerHost([f1, config]);
3603+
const session = createSession(host, /*typingsInstaller*/ undefined, () => { }, cancellationToken, /*throttleWaitMilliseconds*/ 0);
3604+
{
3605+
session.executeCommandSeq(<protocol.OpenRequest>{
3606+
command: "open",
3607+
arguments: { file: f1.path }
3608+
});
3609+
3610+
// send navbar request (normal priority)
3611+
session.executeCommandSeq(<protocol.NavBarRequest>{
3612+
command: "navbar",
3613+
arguments: { file: f1.path }
3614+
});
3615+
3616+
// ensure the nav bar request can be canceled
3617+
verifyExecuteCommandSeqIsCancellable(<protocol.NavBarRequest>{
3618+
command: "navbar",
3619+
arguments: { file: f1.path }
3620+
});
3621+
3622+
// send outlining spans request (normal priority)
3623+
session.executeCommandSeq(<protocol.OutliningSpansRequest>{
3624+
command: "outliningSpans",
3625+
arguments: { file: f1.path }
3626+
});
3627+
3628+
// ensure the outlining spans request can be canceled
3629+
verifyExecuteCommandSeqIsCancellable(<protocol.OutliningSpansRequest>{
3630+
command: "outliningSpans",
3631+
arguments: { file: f1.path }
3632+
});
3633+
}
3634+
3635+
function verifyExecuteCommandSeqIsCancellable<T extends server.protocol.Request>(request: Partial<T>) {
3636+
// Set the next request to be cancellable
3637+
// The cancellation token will cancel the request the third time
3638+
// isCancellationRequested() is called.
3639+
cancellationToken.setRequestToCancel(session.getNextSeq());
3640+
let operationCanceledExceptionThrown = false;
3641+
3642+
try {
3643+
session.executeCommandSeq(request);
3644+
}
3645+
catch (e) {
3646+
assert(e instanceof OperationCanceledException);
3647+
operationCanceledExceptionThrown = true;
3648+
}
3649+
assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request));
3650+
}
3651+
});
35613652
});
35623653

35633654
describe("occurence highlight on string", () => {

src/server/editorServices.ts

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

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

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
@@ -337,7 +337,8 @@ namespace ts.server {
337337
private hrtime: (start?: number[]) => number[],
338338
protected logger: Logger,
339339
protected readonly canUseEvents: boolean,
340-
eventHandler?: ProjectServiceEventHandler) {
340+
eventHandler?: ProjectServiceEventHandler,
341+
private readonly throttleWaitMilliseconds?: number) {
341342

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

src/services/navigationBar.ts

Lines changed: 55 additions & 35 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.
@@ -14,22 +44,36 @@ namespace ts.NavigationBar {
1444
indent: number; // # of parents
1545
}
1646

17-
export function getNavigationBarItems(sourceFile: SourceFile): NavigationBarItem[] {
47+
export function getNavigationBarItems(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationBarItem[] {
48+
curCancellationToken = cancellationToken;
1849
curSourceFile = sourceFile;
19-
const result = map(topLevelItems(rootNavigationBarNode(sourceFile)), convertToTopLevelItem);
20-
curSourceFile = undefined;
21-
return result;
50+
try {
51+
return map(topLevelItems(rootNavigationBarNode(sourceFile)), convertToTopLevelItem);
52+
}
53+
finally {
54+
reset();
55+
}
2256
}
2357

24-
export function getNavigationTree(sourceFile: SourceFile): NavigationTree {
58+
export function getNavigationTree(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationTree {
59+
curCancellationToken = cancellationToken;
2560
curSourceFile = sourceFile;
26-
const result = convertToTree(rootNavigationBarNode(sourceFile));
61+
try {
62+
return convertToTree(rootNavigationBarNode(sourceFile));
63+
}
64+
finally {
65+
reset();
66+
}
67+
}
68+
69+
function reset() {
2770
curSourceFile = undefined;
28-
return result;
71+
curCancellationToken = undefined;
72+
parentsStack = [];
73+
parent = undefined;
74+
emptyChildItemArray = [];
2975
}
3076

31-
// Keep sourceFile handy so we don't have to search for it every time we need to call `getText`.
32-
let curSourceFile: SourceFile;
3377
function nodeText(node: Node): string {
3478
return node.getText(curSourceFile);
3579
}
@@ -47,14 +91,6 @@ namespace ts.NavigationBar {
4791
}
4892
}
4993

50-
/*
51-
For performance, we keep navigation bar parents on a stack rather than passing them through each recursion.
52-
`parent` is the current parent and is *not* stored in parentsStack.
53-
`startNode` sets a new parent and `endNode` returns to the previous parent.
54-
*/
55-
const parentsStack: NavigationBarNode[] = [];
56-
let parent: NavigationBarNode;
57-
5894
function rootNavigationBarNode(sourceFile: SourceFile): NavigationBarNode {
5995
Debug.assert(!parentsStack.length);
6096
const root: NavigationBarNode = { node: sourceFile, additionalNodes: undefined, parent: undefined, children: undefined, indent: 0 };
@@ -111,6 +147,8 @@ namespace ts.NavigationBar {
111147

112148
/** Look for navigation bar items in node's subtree, adding them to the current `parent`. */
113149
function addChildrenRecursively(node: Node): void {
150+
curCancellationToken.throwIfCancellationRequested();
151+
114152
if (!node || isToken(node)) {
115153
return;
116154
}
@@ -487,9 +525,6 @@ namespace ts.NavigationBar {
487525
}
488526
}
489527

490-
// NavigationBarItem requires an array, but will not mutate it, so just give it this for performance.
491-
const emptyChildItemArray: NavigationBarItem[] = [];
492-
493528
function convertToTree(n: NavigationBarNode): NavigationTree {
494529
return {
495530
text: getItemName(n.node),
@@ -610,19 +645,4 @@ namespace ts.NavigationBar {
610645
function isFunctionOrClassExpression(node: Node): boolean {
611646
return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction || node.kind === SyntaxKind.ClassExpression;
612647
}
613-
614-
/**
615-
* Matches all whitespace characters in a string. Eg:
616-
*
617-
* "app.
618-
*
619-
* onactivated"
620-
*
621-
* matches because of the newline, whereas
622-
*
623-
* "app.onactivated"
624-
*
625-
* does not match.
626-
*/
627-
const whiteSpaceRegex = /\s+/g;
628648
}

0 commit comments

Comments
 (0)