carrick 0.3.106 → 0.3.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hook/refresh.js +8 -1
- package/dist/hook/refresh.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +15 -2
- package/sidecar/dist/src/capture/anchors.js +28 -11
- package/sidecar/dist/src/capture/api.d.ts +15 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +9 -0
- package/sidecar/dist/src/capture/check-classify.js +20 -0
- package/sidecar/dist/src/capture/check-fields.d.ts +24 -0
- package/sidecar/dist/src/capture/check-fields.js +54 -36
- package/sidecar/dist/src/capture/check-poison.js +4 -14
- package/sidecar/dist/src/capture/check-union.d.ts +40 -0
- package/sidecar/dist/src/capture/check-union.js +92 -0
- package/sidecar/dist/src/capture/check.js +5 -0
- package/sidecar/dist/src/capture/index.js +138 -80
- package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
- package/sidecar/dist/src/capture/installed-package.js +2 -1
- package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
- package/sidecar/dist/src/capture/outside-root.js +39 -2
- package/sidecar/dist/src/capture/self-check.js +3 -13
- package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
- package/sidecar/dist/src/capture/specifiers.js +25 -0
- package/sidecar/dist/src/definition-resolver.d.ts +5 -8
- package/sidecar/dist/src/definition-resolver.js +5 -7
- package/sidecar/dist/src/function-line-index.d.ts +30 -0
- package/sidecar/dist/src/function-line-index.js +162 -0
- package/sidecar/dist/src/index.d.ts +5 -0
- package/sidecar/dist/src/index.js +62 -9
- package/sidecar/dist/src/infer-timing.d.ts +54 -0
- package/sidecar/dist/src/infer-timing.js +124 -0
- package/sidecar/dist/src/line-index.d.ts +9 -0
- package/sidecar/dist/src/line-index.js +26 -0
- package/sidecar/dist/src/progress.d.ts +22 -0
- package/sidecar/dist/src/progress.js +31 -0
- package/sidecar/dist/src/retype.d.ts +4 -2
- package/sidecar/dist/src/retype.js +10 -23
- package/sidecar/dist/src/type-inferrer.d.ts +238 -12
- package/sidecar/dist/src/type-inferrer.js +772 -168
- package/sidecar/dist/src/types.d.ts +83 -6
- package/sidecar/dist/src/validators.d.ts +54 -16
- package/sidecar/dist/src/validators.js +4 -0
- package/templates/skills/carrick-reuse.md +6 -5
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The function a line names, from an index built once per source file
|
|
3
|
+
* (carrick#1915).
|
|
4
|
+
*
|
|
5
|
+
* A signature request carries only the line its function starts on, and a
|
|
6
|
+
* signature pass sends one request per unannotated slot: thousands of
|
|
7
|
+
* lookups, several per function and many per file. Answering each by walking
|
|
8
|
+
* the file's every node cost the file's size per slot, and was 81% of the
|
|
9
|
+
* pass on a 2,345-file program. Here the file is walked once, over the
|
|
10
|
+
* compiler's own nodes, and each lookup reads the few entries near its line.
|
|
11
|
+
*
|
|
12
|
+
* The index belongs to the compiler's source file node, not to the file's
|
|
13
|
+
* name: replacing a file's text gives it a new node, so a file rewritten for
|
|
14
|
+
* one reading (the unwidened reading, the retype check) and then restored is
|
|
15
|
+
* indexed again each time, and never answered from positions it no longer has.
|
|
16
|
+
*/
|
|
17
|
+
import { SyntaxKind, ts, } from 'ts-morph';
|
|
18
|
+
import { lineIndex } from './line-index.js';
|
|
19
|
+
/** How far, in lines, a function may start from the line that names it. */
|
|
20
|
+
const LINE_TOLERANCE = 2;
|
|
21
|
+
const FUNCTION_KINDS = new Set([
|
|
22
|
+
SyntaxKind.FunctionDeclaration,
|
|
23
|
+
SyntaxKind.ArrowFunction,
|
|
24
|
+
SyntaxKind.FunctionExpression,
|
|
25
|
+
SyntaxKind.MethodDeclaration,
|
|
26
|
+
]);
|
|
27
|
+
/** The kinds ts-morph's `Node.isStatement` accepts. */
|
|
28
|
+
const STATEMENT_KINDS = new Set([
|
|
29
|
+
SyntaxKind.Block,
|
|
30
|
+
SyntaxKind.BreakStatement,
|
|
31
|
+
SyntaxKind.ClassDeclaration,
|
|
32
|
+
SyntaxKind.ContinueStatement,
|
|
33
|
+
SyntaxKind.DebuggerStatement,
|
|
34
|
+
SyntaxKind.DoStatement,
|
|
35
|
+
SyntaxKind.EmptyStatement,
|
|
36
|
+
SyntaxKind.EnumDeclaration,
|
|
37
|
+
SyntaxKind.ExportAssignment,
|
|
38
|
+
SyntaxKind.ExportDeclaration,
|
|
39
|
+
SyntaxKind.ExpressionStatement,
|
|
40
|
+
SyntaxKind.ForInStatement,
|
|
41
|
+
SyntaxKind.ForOfStatement,
|
|
42
|
+
SyntaxKind.ForStatement,
|
|
43
|
+
SyntaxKind.FunctionDeclaration,
|
|
44
|
+
SyntaxKind.IfStatement,
|
|
45
|
+
SyntaxKind.ImportDeclaration,
|
|
46
|
+
SyntaxKind.ImportEqualsDeclaration,
|
|
47
|
+
SyntaxKind.InterfaceDeclaration,
|
|
48
|
+
SyntaxKind.LabeledStatement,
|
|
49
|
+
SyntaxKind.ModuleBlock,
|
|
50
|
+
SyntaxKind.ModuleDeclaration,
|
|
51
|
+
SyntaxKind.NotEmittedStatement,
|
|
52
|
+
SyntaxKind.ReturnStatement,
|
|
53
|
+
SyntaxKind.SwitchStatement,
|
|
54
|
+
SyntaxKind.ThrowStatement,
|
|
55
|
+
SyntaxKind.TryStatement,
|
|
56
|
+
SyntaxKind.TypeAliasDeclaration,
|
|
57
|
+
SyntaxKind.VariableStatement,
|
|
58
|
+
SyntaxKind.WhileStatement,
|
|
59
|
+
SyntaxKind.WithStatement,
|
|
60
|
+
]);
|
|
61
|
+
const indexes = new WeakMap();
|
|
62
|
+
/**
|
|
63
|
+
* The function whose declaration starts at, or within `LINE_TOLERANCE` lines
|
|
64
|
+
* of, the given line: the closest, then the smallest, then the first in the
|
|
65
|
+
* file. A function starting after the line is passed over when a statement
|
|
66
|
+
* that opens on the line or after it, on a line before the function's, does
|
|
67
|
+
* not contain the function. Undefined when no function is left.
|
|
68
|
+
*
|
|
69
|
+
* These are `TypeInferrer.findFunctionByLine`'s rules, which says why each is
|
|
70
|
+
* there; this is where they are computed.
|
|
71
|
+
*/
|
|
72
|
+
export function functionAtLine(sourceFile, line) {
|
|
73
|
+
if (!Number.isFinite(line))
|
|
74
|
+
return undefined;
|
|
75
|
+
const index = indexOf(sourceFile.compilerNode);
|
|
76
|
+
/** Statements opening inside the forward window. */
|
|
77
|
+
const windowStatements = [];
|
|
78
|
+
for (let at = Math.ceil(line); at <= line + LINE_TOLERANCE; at++) {
|
|
79
|
+
const opening = index.statementsByLine.get(at);
|
|
80
|
+
if (opening)
|
|
81
|
+
windowStatements.push(...opening);
|
|
82
|
+
}
|
|
83
|
+
const separatedFromAnchor = (fn) => windowStatements.some((statement) => statement.line < fn.line && !(statement.start <= fn.start && statement.end >= fn.end));
|
|
84
|
+
let best;
|
|
85
|
+
for (let at = Math.ceil(line - LINE_TOLERANCE); at <= line + LINE_TOLERANCE; at++) {
|
|
86
|
+
for (const fn of index.functionsByLine.get(at) ?? []) {
|
|
87
|
+
if (fn.line > line && separatedFromAnchor(fn))
|
|
88
|
+
continue;
|
|
89
|
+
if (best === undefined || isBetter(fn, best, line))
|
|
90
|
+
best = fn;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return best ? wrapped(sourceFile, best) : undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Closer to the line, then smaller, then earlier in the file. */
|
|
96
|
+
function isBetter(fn, best, line) {
|
|
97
|
+
const delta = Math.abs(fn.line - line);
|
|
98
|
+
const bestDelta = Math.abs(best.line - line);
|
|
99
|
+
if (delta !== bestDelta)
|
|
100
|
+
return delta < bestDelta;
|
|
101
|
+
const size = fn.end - fn.start;
|
|
102
|
+
const bestSize = best.end - best.start;
|
|
103
|
+
if (size !== bestSize)
|
|
104
|
+
return size < bestSize;
|
|
105
|
+
return fn.order < best.order;
|
|
106
|
+
}
|
|
107
|
+
function indexOf(file) {
|
|
108
|
+
let index = indexes.get(file);
|
|
109
|
+
if (!index) {
|
|
110
|
+
index = buildIndex(file);
|
|
111
|
+
indexes.set(file, index);
|
|
112
|
+
}
|
|
113
|
+
return index;
|
|
114
|
+
}
|
|
115
|
+
function buildIndex(file) {
|
|
116
|
+
// Lines as ts-morph's `getStartLineNumber` counts them, which is what the
|
|
117
|
+
// scanner's line numbers were matched against.
|
|
118
|
+
const lineAt = lineIndex(file.text);
|
|
119
|
+
const functionsByLine = new Map();
|
|
120
|
+
const statementsByLine = new Map();
|
|
121
|
+
let order = 0;
|
|
122
|
+
const visit = (node) => {
|
|
123
|
+
const isFunction = FUNCTION_KINDS.has(node.kind);
|
|
124
|
+
const isStatement = STATEMENT_KINDS.has(node.kind);
|
|
125
|
+
if (isFunction || isStatement) {
|
|
126
|
+
const start = node.getStart(file);
|
|
127
|
+
const extent = { start, end: node.end, line: lineAt(start) };
|
|
128
|
+
if (isFunction) {
|
|
129
|
+
pushAt(functionsByLine, extent.line, { ...extent, node, order: order++ });
|
|
130
|
+
}
|
|
131
|
+
if (isStatement) {
|
|
132
|
+
pushAt(statementsByLine, extent.line, extent);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
ts.forEachChild(node, visit);
|
|
136
|
+
};
|
|
137
|
+
ts.forEachChild(file, visit);
|
|
138
|
+
return { functionsByLine, statementsByLine };
|
|
139
|
+
}
|
|
140
|
+
function pushAt(byLine, line, entry) {
|
|
141
|
+
const entries = byLine.get(line);
|
|
142
|
+
if (entries)
|
|
143
|
+
entries.push(entry);
|
|
144
|
+
else
|
|
145
|
+
byLine.set(line, [entry]);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The ts-morph node for an indexed function. Only this one is wrapped: the
|
|
149
|
+
* deepest node at the function's first character is the function or sits
|
|
150
|
+
* inside it, so its ancestors lead there, and the rest of the file's nodes are
|
|
151
|
+
* never given wrappers they would keep for the life of the project.
|
|
152
|
+
*/
|
|
153
|
+
function wrapped(sourceFile, fn) {
|
|
154
|
+
let node = sourceFile.getDescendantAtPos(fn.start);
|
|
155
|
+
while (node && node.compilerNode !== fn.node) {
|
|
156
|
+
node = node.getParent();
|
|
157
|
+
}
|
|
158
|
+
// An invariant, not a case: no shape is known where the climb misses. If
|
|
159
|
+
// one exists, the answer is still the function the index chose.
|
|
160
|
+
node ??= sourceFile.getFirstDescendant((descendant) => descendant.compilerNode === fn.node);
|
|
161
|
+
return node;
|
|
162
|
+
}
|
|
@@ -10,5 +10,10 @@
|
|
|
10
10
|
* - stdout is ONLY for JSON responses
|
|
11
11
|
* - stderr is for logging
|
|
12
12
|
* - Process stays alive between requests (warm standby)
|
|
13
|
+
* - One request at a time. A handler that runs the compiler blocks the event
|
|
14
|
+
* loop until it returns, so a request sent meanwhile waits in the pipe, and
|
|
15
|
+
* nothing can cancel the one that is running. The scanner kills a process
|
|
16
|
+
* that goes silent past its deadline; a long handler stays alive by writing
|
|
17
|
+
* `progress` frames as it finishes units of work (writeProgress).
|
|
13
18
|
*/
|
|
14
19
|
export {};
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* - stdout is ONLY for JSON responses
|
|
11
11
|
* - stderr is for logging
|
|
12
12
|
* - Process stays alive between requests (warm standby)
|
|
13
|
+
* - One request at a time. A handler that runs the compiler blocks the event
|
|
14
|
+
* loop until it returns, so a request sent meanwhile waits in the pipe, and
|
|
15
|
+
* nothing can cancel the one that is running. The scanner kills a process
|
|
16
|
+
* that goes silent past its deadline; a long handler stays alive by writing
|
|
17
|
+
* `progress` frames as it finishes units of work (writeProgress).
|
|
13
18
|
*/
|
|
14
19
|
import * as path from 'node:path';
|
|
15
20
|
import * as readline from 'node:readline';
|
|
@@ -21,6 +26,8 @@ import { DefinitionResolver } from './definition-resolver.js';
|
|
|
21
26
|
import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
|
|
22
27
|
import { Retyper } from './retype.js';
|
|
23
28
|
import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
|
|
29
|
+
import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
|
|
30
|
+
import { mergeInferTimings } from './infer-timing.js';
|
|
24
31
|
// ===========================================================================
|
|
25
32
|
// Module-level state
|
|
26
33
|
// ===========================================================================
|
|
@@ -32,6 +39,13 @@ let initTimeMs = null;
|
|
|
32
39
|
* the service's files (carrick#1604).
|
|
33
40
|
*/
|
|
34
41
|
let components = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* Reads a capture stub's own declaration tree and nothing of the init'd
|
|
44
|
+
* project, so it is not one of the project's components: taking it from them
|
|
45
|
+
* would build the service's whole program to answer about the stub
|
|
46
|
+
* (carrick#1927).
|
|
47
|
+
*/
|
|
48
|
+
const definitionResolver = new DefinitionResolver();
|
|
35
49
|
/**
|
|
36
50
|
* Get the project-backed components, building the project if this is the
|
|
37
51
|
* first request that needs it.
|
|
@@ -61,7 +75,6 @@ function projectComponents(key = '') {
|
|
|
61
75
|
built = {
|
|
62
76
|
typeBundler: new TypeBundler({ project, repoRoot }),
|
|
63
77
|
typeInferrer,
|
|
64
|
-
definitionResolver: new DefinitionResolver({ project }),
|
|
65
78
|
// Locates calls exactly as `infer` does, so it rewrites the node the
|
|
66
79
|
// consumer's published type came from (carrick#1491).
|
|
67
80
|
// The walk is typed against the capture bundle's compiler copy and
|
|
@@ -168,7 +181,11 @@ function handleBundle(request) {
|
|
|
168
181
|
log(`Bundling ${request.symbols.length} symbol(s)`);
|
|
169
182
|
// A symbol named by a path is bundled from the program of the project
|
|
170
183
|
// that owns that file (carrick#1604).
|
|
171
|
-
const results = [...byProject(request.symbols, (symbol) => symbol.source_file)].map(([key, symbols]) =>
|
|
184
|
+
const results = [...byProject(request.symbols, (symbol) => symbol.source_file)].map(([key, symbols]) => {
|
|
185
|
+
const { typeBundler } = projectComponents(key);
|
|
186
|
+
writeProgress(request.request_id, 'bundle', 'program ready');
|
|
187
|
+
return typeBundler.bundle(symbols);
|
|
188
|
+
});
|
|
172
189
|
const result = results.length === 1 ? results[0] : mergeBundles(results);
|
|
173
190
|
if (!result.success) {
|
|
174
191
|
return {
|
|
@@ -208,6 +225,10 @@ function handleBundle(request) {
|
|
|
208
225
|
function handleCaptureV2(request) {
|
|
209
226
|
try {
|
|
210
227
|
log(`capture_v2 for service '${request.service_name}' (${request.anchors.length} anchor(s))`);
|
|
228
|
+
// A frame for each stage as the capture reaches it, and within a stage
|
|
229
|
+
// (one report per anchor) at most one per interval.
|
|
230
|
+
let stage;
|
|
231
|
+
const withinStage = atMostEvery(PROGRESS_INTERVAL_MS, (phase, message) => writeProgress(request.request_id, phase, message));
|
|
211
232
|
const result = captureStub({
|
|
212
233
|
repoRoot: request.repo_root,
|
|
213
234
|
serviceName: request.service_name,
|
|
@@ -215,6 +236,12 @@ function handleCaptureV2(request) {
|
|
|
215
236
|
outDir: request.out_dir,
|
|
216
237
|
tsconfigPath: request.tsconfig_path,
|
|
217
238
|
scanRoot: request.scan_root,
|
|
239
|
+
onProgress: (phase, message) => {
|
|
240
|
+
if (phase === stage)
|
|
241
|
+
return withinStage(phase, message);
|
|
242
|
+
stage = phase;
|
|
243
|
+
writeProgress(request.request_id, phase, message);
|
|
244
|
+
},
|
|
218
245
|
});
|
|
219
246
|
return {
|
|
220
247
|
request_id: request.request_id,
|
|
@@ -284,7 +311,13 @@ async function handleCheckV2Async(request) {
|
|
|
284
311
|
function handleInfer(request) {
|
|
285
312
|
try {
|
|
286
313
|
log(`Inferring ${request.requests.length} type(s)`);
|
|
287
|
-
|
|
314
|
+
// One count for the request, whichever programs its items span.
|
|
315
|
+
let done = 0;
|
|
316
|
+
const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'infer', `${done} of ${request.requests.length}`));
|
|
317
|
+
const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => projectComponents(key).typeInferrer.infer(items, request.extraction_config, () => {
|
|
318
|
+
done += 1;
|
|
319
|
+
report();
|
|
320
|
+
}));
|
|
288
321
|
const result = results.length === 1
|
|
289
322
|
? results[0]
|
|
290
323
|
: (() => {
|
|
@@ -300,6 +333,9 @@ function handleInfer(request) {
|
|
|
300
333
|
request_id: request.request_id,
|
|
301
334
|
status: result.success ? 'success' : 'error',
|
|
302
335
|
inferred_types: result.inferred_types,
|
|
336
|
+
// Beside the answers and never in them: what the requests cost, for
|
|
337
|
+
// the caller's log (carrick#1985).
|
|
338
|
+
infer_timing: mergeInferTimings(results.map((r) => r.timing)),
|
|
303
339
|
errors: result.errors,
|
|
304
340
|
};
|
|
305
341
|
}
|
|
@@ -329,11 +365,19 @@ function handleRetypeCheck(request) {
|
|
|
329
365
|
const groups = byProject(request.items.map((item, index) => ({ item, index })), ({ item }) => item.file_path);
|
|
330
366
|
const deadline = performance.now() + budget;
|
|
331
367
|
const outcomes = new Array(request.items.length);
|
|
368
|
+
// One count for the request, whichever programs its items span, reported
|
|
369
|
+
// as `infer` reports its batch (carrick#1914, carrick#1945).
|
|
370
|
+
let judged = 0;
|
|
371
|
+
const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'retype', `${judged} of ${request.items.length}`));
|
|
372
|
+
const onJudged = () => {
|
|
373
|
+
judged += 1;
|
|
374
|
+
report();
|
|
375
|
+
};
|
|
332
376
|
for (const [key, entries] of groups) {
|
|
333
377
|
// One budget for the request, whichever programs it spans.
|
|
334
378
|
const remaining = groups.size === 1 ? budget : Math.max(0, deadline - performance.now());
|
|
335
379
|
projectComponents(key)
|
|
336
|
-
.retyper.run(entries.map(({ item }) => item), remaining)
|
|
380
|
+
.retyper.run(entries.map(({ item }) => item), remaining, onJudged)
|
|
337
381
|
.forEach((outcome, position) => {
|
|
338
382
|
outcomes[entries[position].index] = outcome;
|
|
339
383
|
});
|
|
@@ -388,6 +432,8 @@ function handleVerifyLibraryClaims(request) {
|
|
|
388
432
|
const started = performance.now();
|
|
389
433
|
try {
|
|
390
434
|
const { claimsVerifier } = projectComponents();
|
|
435
|
+
// The verifier's budget starts now, and so does the scanner's wait.
|
|
436
|
+
writeProgress(request.request_id, 'verify_library_claims', 'program ready');
|
|
391
437
|
const fromDir = path.resolve(projectLoader.getRepoRoot(), request.from_dir);
|
|
392
438
|
log(`Verifying ${request.checks.length} library claim(s) from ${fromDir}`);
|
|
393
439
|
const { semantics, modules } = claimsVerifier.run(fromDir, request.checks, request.budget_ms ?? SEMANTICS_BUDGET_MS);
|
|
@@ -428,12 +474,13 @@ function handleListLibrarySurface(request) {
|
|
|
428
474
|
}
|
|
429
475
|
/**
|
|
430
476
|
* Handle the 'resolve_definitions' action - resolve surface aliases from a
|
|
431
|
-
* v2 capture stub package's declaration tree.
|
|
477
|
+
* v2 capture stub package's declaration tree. Stateless, as `capture_v2` is:
|
|
478
|
+
* it needs no init and builds no project but the stub's own.
|
|
432
479
|
*/
|
|
433
480
|
function handleResolveDefinitions(request) {
|
|
434
481
|
try {
|
|
435
482
|
log(`Resolving ${request.aliases.length} type alias(es) from ${request.stub_dir}`);
|
|
436
|
-
const results =
|
|
483
|
+
const results = definitionResolver.resolveFromStub(request.stub_dir, request.aliases);
|
|
437
484
|
return {
|
|
438
485
|
request_id: request.request_id,
|
|
439
486
|
status: 'success',
|
|
@@ -530,9 +577,15 @@ function writeResponse(response) {
|
|
|
530
577
|
process.stdout.write(json + '\n');
|
|
531
578
|
}
|
|
532
579
|
/**
|
|
533
|
-
* Write a non-terminal progress
|
|
534
|
-
*
|
|
535
|
-
*
|
|
580
|
+
* Write a non-terminal progress frame. Distinct `status: 'progress'` so
|
|
581
|
+
* clients skip it and wait for the terminal success/error frame; the scanner
|
|
582
|
+
* restarts its deadline for the request on each one (carrick#1914).
|
|
583
|
+
*
|
|
584
|
+
* A synchronous handler calls this between units of its work, with the event
|
|
585
|
+
* loop blocked. The frame still leaves at once: a write this small to a pipe
|
|
586
|
+
* with room in it completes inside the call, and the reader on the other end
|
|
587
|
+
* drains the pipe on its own thread, so there is room. It is written through
|
|
588
|
+
* the same stream as every answer, so a frame can never land inside one.
|
|
536
589
|
*/
|
|
537
590
|
function writeProgress(requestId, phase, message) {
|
|
538
591
|
process.stdout.write(JSON.stringify({ request_id: requestId, status: 'progress', phase, message }) + '\n');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which requests of an `infer` batch were slow, and at what (carrick#1985).
|
|
3
|
+
*
|
|
4
|
+
* A pass can send thousands of requests and spend most of its time in a few
|
|
5
|
+
* of them. The inferrer times every request; this module turns those timings
|
|
6
|
+
* into what the batch's answer carries: how many there were and what they
|
|
7
|
+
* add up to, the slowest few by name, the request that printed the longest
|
|
8
|
+
* type, and the requests that were the first asked of their file, apart.
|
|
9
|
+
*
|
|
10
|
+
* A request's time is also told apart by what it went on: the compiler call
|
|
11
|
+
* that computes the type, and the print of that type to text. The two have
|
|
12
|
+
* different remedies, and a printed length alone cannot say which one a slow
|
|
13
|
+
* request was: a type of four characters can take seconds to compute, and a
|
|
14
|
+
* type of a million can take half a second to print.
|
|
15
|
+
*
|
|
16
|
+
* It measures and names. It changes no answer and holds no type text.
|
|
17
|
+
*/
|
|
18
|
+
import type { InferSlotTiming, InferTiming } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* How many of its slowest requests a batch's answer names.
|
|
21
|
+
*
|
|
22
|
+
* Enough for a reader adding batches up to say what the slowest hundredth of
|
|
23
|
+
* a whole pass took: a pass's slowest requests are among its batches'
|
|
24
|
+
* slowest, and the last one a batch names is the most any it left out can
|
|
25
|
+
* have taken.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SLOWEST_SLOTS = 25;
|
|
28
|
+
/** How long something that started at `startedMs` has taken by `nowMs`. */
|
|
29
|
+
export declare function elapsedMs(startedMs: number, nowMs: number): number;
|
|
30
|
+
/**
|
|
31
|
+
* The parts of a request's time that are told apart: `type` is the compiler
|
|
32
|
+
* call that computes a type, `print` is the print of a type to text.
|
|
33
|
+
*/
|
|
34
|
+
export type TimedPhase = 'type' | 'print';
|
|
35
|
+
/**
|
|
36
|
+
* Run `work`, and count how long it took as `phase`.
|
|
37
|
+
*
|
|
38
|
+
* The count is the process's, not a request's: whoever wants one request's
|
|
39
|
+
* share reads `phaseClock` before the request and after it. One request runs
|
|
40
|
+
* at a time, so the difference is that request's and nobody else's.
|
|
41
|
+
*/
|
|
42
|
+
export declare function timedPhase<T>(phase: TimedPhase, work: () => T): T;
|
|
43
|
+
/** What each phase has come to so far, in milliseconds. */
|
|
44
|
+
export declare function phaseClock(): Readonly<Record<TimedPhase, number>>;
|
|
45
|
+
/**
|
|
46
|
+
* What a batch's answer says of the time its requests took. `slots` holds one
|
|
47
|
+
* timing per request, in the order the batch was done with them.
|
|
48
|
+
*/
|
|
49
|
+
export declare function inferTiming(slots: readonly InferSlotTiming[]): InferTiming;
|
|
50
|
+
/**
|
|
51
|
+
* One timing for a request several projects answered, each with its own:
|
|
52
|
+
* the counts and the times added up, and the slowest of all of them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function mergeInferTimings(parts: readonly InferTiming[]): InferTiming;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which requests of an `infer` batch were slow, and at what (carrick#1985).
|
|
3
|
+
*
|
|
4
|
+
* A pass can send thousands of requests and spend most of its time in a few
|
|
5
|
+
* of them. The inferrer times every request; this module turns those timings
|
|
6
|
+
* into what the batch's answer carries: how many there were and what they
|
|
7
|
+
* add up to, the slowest few by name, the request that printed the longest
|
|
8
|
+
* type, and the requests that were the first asked of their file, apart.
|
|
9
|
+
*
|
|
10
|
+
* A request's time is also told apart by what it went on: the compiler call
|
|
11
|
+
* that computes the type, and the print of that type to text. The two have
|
|
12
|
+
* different remedies, and a printed length alone cannot say which one a slow
|
|
13
|
+
* request was: a type of four characters can take seconds to compute, and a
|
|
14
|
+
* type of a million can take half a second to print.
|
|
15
|
+
*
|
|
16
|
+
* It measures and names. It changes no answer and holds no type text.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* How many of its slowest requests a batch's answer names.
|
|
20
|
+
*
|
|
21
|
+
* Enough for a reader adding batches up to say what the slowest hundredth of
|
|
22
|
+
* a whole pass took: a pass's slowest requests are among its batches'
|
|
23
|
+
* slowest, and the last one a batch names is the most any it left out can
|
|
24
|
+
* have taken.
|
|
25
|
+
*/
|
|
26
|
+
export const SLOWEST_SLOTS = 25;
|
|
27
|
+
/** Milliseconds to the microsecond: sums of these stay readable. */
|
|
28
|
+
const rounded = (ms) => Math.round(ms * 1000) / 1000;
|
|
29
|
+
/** How long something that started at `startedMs` has taken by `nowMs`. */
|
|
30
|
+
export function elapsedMs(startedMs, nowMs) {
|
|
31
|
+
return rounded(nowMs - startedMs);
|
|
32
|
+
}
|
|
33
|
+
/** What this process has spent in each phase since it started, in milliseconds. */
|
|
34
|
+
const spent = { type: 0, print: 0 };
|
|
35
|
+
/**
|
|
36
|
+
* Run `work`, and count how long it took as `phase`.
|
|
37
|
+
*
|
|
38
|
+
* The count is the process's, not a request's: whoever wants one request's
|
|
39
|
+
* share reads `phaseClock` before the request and after it. One request runs
|
|
40
|
+
* at a time, so the difference is that request's and nobody else's.
|
|
41
|
+
*/
|
|
42
|
+
export function timedPhase(phase, work) {
|
|
43
|
+
const started = performance.now();
|
|
44
|
+
try {
|
|
45
|
+
return work();
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
spent[phase] += performance.now() - started;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** What each phase has come to so far, in milliseconds. */
|
|
52
|
+
export function phaseClock() {
|
|
53
|
+
return { ...spent };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The `count` slowest of `slots`, slowest first. Requests that took the same
|
|
57
|
+
* time keep the order they are in.
|
|
58
|
+
*/
|
|
59
|
+
function slowestOf(slots, count) {
|
|
60
|
+
return [...slots].sort((a, b) => b.ms - a.ms).slice(0, count);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The one of `slots` that printed the longest type, the earliest of those
|
|
64
|
+
* that tie; undefined when none printed anything.
|
|
65
|
+
*/
|
|
66
|
+
function longestPrintedOf(slots) {
|
|
67
|
+
let longest;
|
|
68
|
+
for (const slot of slots) {
|
|
69
|
+
if (slot.printed_length > (longest?.printed_length ?? 0))
|
|
70
|
+
longest = slot;
|
|
71
|
+
}
|
|
72
|
+
return longest;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* What a batch's answer says of the time its requests took. `slots` holds one
|
|
76
|
+
* timing per request, in the order the batch was done with them.
|
|
77
|
+
*/
|
|
78
|
+
export function inferTiming(slots) {
|
|
79
|
+
let slotsMs = 0;
|
|
80
|
+
let typeMs = 0;
|
|
81
|
+
let printMs = 0;
|
|
82
|
+
let firstInFile = 0;
|
|
83
|
+
let firstInFileMs = 0;
|
|
84
|
+
for (const slot of slots) {
|
|
85
|
+
slotsMs += slot.ms;
|
|
86
|
+
typeMs += slot.type_ms;
|
|
87
|
+
printMs += slot.print_ms;
|
|
88
|
+
if (slot.first_in_file) {
|
|
89
|
+
firstInFile += 1;
|
|
90
|
+
firstInFileMs += slot.ms;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const longest = longestPrintedOf(slots);
|
|
94
|
+
return {
|
|
95
|
+
slots: slots.length,
|
|
96
|
+
slots_ms: rounded(slotsMs),
|
|
97
|
+
type_ms: rounded(typeMs),
|
|
98
|
+
print_ms: rounded(printMs),
|
|
99
|
+
first_in_file_slots: firstInFile,
|
|
100
|
+
first_in_file_ms: rounded(firstInFileMs),
|
|
101
|
+
slowest: slowestOf(slots, SLOWEST_SLOTS),
|
|
102
|
+
...(longest ? { longest_printed: longest } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One timing for a request several projects answered, each with its own:
|
|
107
|
+
* the counts and the times added up, and the slowest of all of them.
|
|
108
|
+
*/
|
|
109
|
+
export function mergeInferTimings(parts) {
|
|
110
|
+
if (parts.length === 1)
|
|
111
|
+
return parts[0];
|
|
112
|
+
const sum = (read) => parts.reduce((total, part) => total + read(part), 0);
|
|
113
|
+
const longest = longestPrintedOf(parts.flatMap((part) => part.longest_printed ?? []));
|
|
114
|
+
return {
|
|
115
|
+
slots: sum((part) => part.slots),
|
|
116
|
+
slots_ms: rounded(sum((part) => part.slots_ms)),
|
|
117
|
+
type_ms: rounded(sum((part) => part.type_ms)),
|
|
118
|
+
print_ms: rounded(sum((part) => part.print_ms)),
|
|
119
|
+
first_in_file_slots: sum((part) => part.first_in_file_slots),
|
|
120
|
+
first_in_file_ms: rounded(sum((part) => part.first_in_file_ms)),
|
|
121
|
+
slowest: slowestOf(parts.flatMap((part) => part.slowest), SLOWEST_SLOTS),
|
|
122
|
+
...(longest ? { longest_printed: longest } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 1-based line of a position in a text, counted by line feeds alone: one
|
|
3
|
+
* more than the `\n`s before the position. That is the count ts-morph's
|
|
4
|
+
* `getStartLineNumber` makes. A lone carriage return or a Unicode line
|
|
5
|
+
* separator ends a line for the compiler and not here.
|
|
6
|
+
*
|
|
7
|
+
* The text is read once; each lookup after that is a binary search.
|
|
8
|
+
*/
|
|
9
|
+
export declare function lineIndex(text: string): (pos: number) => number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 1-based line of a position in a text, counted by line feeds alone: one
|
|
3
|
+
* more than the `\n`s before the position. That is the count ts-morph's
|
|
4
|
+
* `getStartLineNumber` makes. A lone carriage return or a Unicode line
|
|
5
|
+
* separator ends a line for the compiler and not here.
|
|
6
|
+
*
|
|
7
|
+
* The text is read once; each lookup after that is a binary search.
|
|
8
|
+
*/
|
|
9
|
+
export function lineIndex(text) {
|
|
10
|
+
const starts = [0];
|
|
11
|
+
for (let i = 0; i < text.length; i++)
|
|
12
|
+
if (text[i] === '\n')
|
|
13
|
+
starts.push(i + 1);
|
|
14
|
+
return (pos) => {
|
|
15
|
+
let lo = 0;
|
|
16
|
+
let hi = starts.length - 1;
|
|
17
|
+
while (lo < hi) {
|
|
18
|
+
const mid = (lo + hi + 1) >> 1;
|
|
19
|
+
if (starts[mid] <= pos)
|
|
20
|
+
lo = mid;
|
|
21
|
+
else
|
|
22
|
+
hi = mid - 1;
|
|
23
|
+
}
|
|
24
|
+
return lo + 1;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacing for the `progress` frames a long request writes (carrick#1914).
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The least time between two progress frames of one request.
|
|
6
|
+
*
|
|
7
|
+
* The scanner's deadline for a request is how long the sidecar may stay
|
|
8
|
+
* silent about it, so a frame has to say that a unit of work finished, and
|
|
9
|
+
* has to be far more frequent than that deadline. It does not have to follow
|
|
10
|
+
* every unit: a batch can finish thousands in a second.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PROGRESS_INTERVAL_MS = 1500;
|
|
13
|
+
/**
|
|
14
|
+
* `report`, held to one call per `intervalMs`: the first call goes through at
|
|
15
|
+
* once, and a call made sooner than `intervalMs` after the last one that went
|
|
16
|
+
* through is dropped.
|
|
17
|
+
*
|
|
18
|
+
* It is called by the work, between units, and never by a timer. A handler
|
|
19
|
+
* that stops finishing units stops reporting, which is what the reader on the
|
|
20
|
+
* other end is waiting to notice.
|
|
21
|
+
*/
|
|
22
|
+
export declare function atMostEvery<Args extends unknown[]>(intervalMs: number, report: (...args: Args) => void, now?: () => number): (...args: Args) => void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacing for the `progress` frames a long request writes (carrick#1914).
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The least time between two progress frames of one request.
|
|
6
|
+
*
|
|
7
|
+
* The scanner's deadline for a request is how long the sidecar may stay
|
|
8
|
+
* silent about it, so a frame has to say that a unit of work finished, and
|
|
9
|
+
* has to be far more frequent than that deadline. It does not have to follow
|
|
10
|
+
* every unit: a batch can finish thousands in a second.
|
|
11
|
+
*/
|
|
12
|
+
export const PROGRESS_INTERVAL_MS = 1500;
|
|
13
|
+
/**
|
|
14
|
+
* `report`, held to one call per `intervalMs`: the first call goes through at
|
|
15
|
+
* once, and a call made sooner than `intervalMs` after the last one that went
|
|
16
|
+
* through is dropped.
|
|
17
|
+
*
|
|
18
|
+
* It is called by the work, between units, and never by a timer. A handler
|
|
19
|
+
* that stops finishing units stops reporting, which is what the reader on the
|
|
20
|
+
* other end is waiting to notice.
|
|
21
|
+
*/
|
|
22
|
+
export function atMostEvery(intervalMs, report, now = () => performance.now()) {
|
|
23
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
24
|
+
return (...args) => {
|
|
25
|
+
const at = now();
|
|
26
|
+
if (at - last < intervalMs)
|
|
27
|
+
return;
|
|
28
|
+
last = at;
|
|
29
|
+
report(...args);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -53,9 +53,11 @@ export declare class Retyper {
|
|
|
53
53
|
* Judge every item, spending at most `budgetMs`. Each item rebuilds the
|
|
54
54
|
* program at least twice, so a consumer with many calls could otherwise
|
|
55
55
|
* outrun the caller's read deadline and lose every answer; the items the
|
|
56
|
-
* budget does not reach abstain and say so.
|
|
56
|
+
* budget does not reach abstain and say so. `onJudged` is called after each
|
|
57
|
+
* item the budget reached, so the caller can report progress while the
|
|
58
|
+
* event loop is blocked (carrick#1945).
|
|
57
59
|
*/
|
|
58
|
-
run(items: RetypeItem[], budgetMs: number): RetypeOutcome[];
|
|
60
|
+
run(items: RetypeItem[], budgetMs: number, onJudged?: () => void): RetypeOutcome[];
|
|
59
61
|
private runOne;
|
|
60
62
|
/**
|
|
61
63
|
* The rewrite that states the producer's UNWIDENED return (carrick#1516),
|