@typeslayer/analyze-trace 0.0.0 → 0.1.0

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.
@@ -0,0 +1,44 @@
1
+ import {
2
+ packageNameRegex,
3
+ type TraceEvent,
4
+ type TraceJsonSchema,
5
+ } from "@typeslayer/validate";
6
+ import type { NodeModulePaths } from "./utils";
7
+
8
+ export function getNodeModulePaths(
9
+ traceJson: TraceJsonSchema,
10
+ ): NodeModulePaths {
11
+ const nodeModulePaths: NodeModulePaths = {};
12
+ traceJson.forEach((event: TraceEvent) => {
13
+ if (event.name !== "findSourceFile") {
14
+ return;
15
+ }
16
+ const path = event.args.fileName;
17
+ if (path) {
18
+ while (true) {
19
+ const match = packageNameRegex.exec(path);
20
+ if (!match) {
21
+ break;
22
+ }
23
+ const packageName = match[1];
24
+
25
+ const packagePath = match.input.substring(
26
+ 0,
27
+ match.index + match[0].length,
28
+ );
29
+
30
+ if (packageName in nodeModulePaths) {
31
+ const paths = nodeModulePaths[packageName];
32
+ if (paths && paths.indexOf(packagePath) < 0) {
33
+ // Usually contains exactly one element
34
+ paths.push(packagePath);
35
+ }
36
+ } else {
37
+ nodeModulePaths[packageName] = [packagePath];
38
+ }
39
+ }
40
+ }
41
+ });
42
+
43
+ return nodeModulePaths;
44
+ }
package/src/spans.ts ADDED
@@ -0,0 +1,137 @@
1
+ import {
2
+ eventPhase,
3
+ type TraceEvent,
4
+ type TraceJsonSchema,
5
+ } from "@typeslayer/validate";
6
+ import type {
7
+ AnalyzeTraceOptions,
8
+ EventSpan,
9
+ Microseconds,
10
+ ParseResult,
11
+ } from "./utils";
12
+
13
+ /*
14
+ * This function takes an array of trace events and converts them into spans.
15
+ */
16
+ export function createSpans(traceFile: TraceJsonSchema): ParseResult {
17
+ // Sorted in increasing order of start time (even when below timestamp resolution)
18
+ const unclosedStack: TraceEvent[] = [];
19
+
20
+ // Sorted in increasing order of end time, then increasing order of start time (even when below timestamp resolution)
21
+ const spans: EventSpan[] = [];
22
+
23
+ traceFile.forEach((event: TraceEvent) => {
24
+ switch (event.ph) {
25
+ case eventPhase.begin:
26
+ unclosedStack.push(event);
27
+ return;
28
+
29
+ case eventPhase.end: {
30
+ const beginEvent = unclosedStack.pop();
31
+ if (!beginEvent) {
32
+ throw new Error("Unmatched end event");
33
+ }
34
+ spans.push({
35
+ event: beginEvent,
36
+ start: beginEvent.ts,
37
+ end: event.ts,
38
+ duration: event.ts - beginEvent.ts,
39
+ children: [],
40
+ });
41
+ break;
42
+ }
43
+
44
+ case eventPhase.complete: {
45
+ const start = event.ts;
46
+ const duration = event.dur ?? 0;
47
+ spans.push({
48
+ event,
49
+ start,
50
+ end: start + duration,
51
+ duration,
52
+ children: [],
53
+ });
54
+ break;
55
+ }
56
+
57
+ case eventPhase.instantGlobal:
58
+ case eventPhase.metadata:
59
+ return;
60
+
61
+ default:
62
+ event satisfies never;
63
+ }
64
+ });
65
+
66
+ const parseResult: ParseResult = {
67
+ firstSpanStart: Math.min(...spans.map(span => span.start)),
68
+ lastSpanEnd: Math.max(...spans.map(span => span.end)),
69
+ spans,
70
+ unclosedStack,
71
+ };
72
+ return parseResult;
73
+ }
74
+
75
+ export function createSpanTree(
76
+ parseResult: ParseResult,
77
+ options: AnalyzeTraceOptions,
78
+ ): EventSpan {
79
+ const { firstSpanStart, lastSpanEnd, spans, unclosedStack } = parseResult;
80
+
81
+ // Add unclosed events to the spans
82
+ for (let i = unclosedStack.length - 1; i >= 0; i--) {
83
+ const event = unclosedStack[i];
84
+ const start = event.ts;
85
+ const end = lastSpanEnd;
86
+ spans.push({
87
+ event,
88
+ start,
89
+ end,
90
+ duration: end - start,
91
+ children: [],
92
+ });
93
+ }
94
+
95
+ spans.sort((a, b) => a.start - b.start);
96
+
97
+ const root: EventSpan = {
98
+ event: {
99
+ name: "root",
100
+ cat: "program",
101
+ },
102
+ start: firstSpanStart,
103
+ end: lastSpanEnd,
104
+ duration: lastSpanEnd - firstSpanStart,
105
+ children: [],
106
+ };
107
+ const stack = [root];
108
+
109
+ for (const span of spans) {
110
+ let i = stack.length - 1;
111
+ for (; i > 0; i--) {
112
+ // No need to check root at stack[0]
113
+ const curr = stack[i];
114
+ if (curr.end > span.start) {
115
+ // Pop down to parent
116
+ stack.length = i + 1;
117
+ break;
118
+ }
119
+ }
120
+
121
+ /** Microseconds */
122
+ const thresholdDuration: Microseconds = options.forceMillis * 1000;
123
+ const isAboveThresholdDuration = span.duration >= thresholdDuration;
124
+
125
+ const parent = stack[i];
126
+ const parentDuration = parent.end - parent.start;
127
+ const isSignificantPortionOfParent =
128
+ span.duration >= parentDuration * options.minSpanParentPercentage;
129
+
130
+ if (isAboveThresholdDuration || isSignificantPortionOfParent) {
131
+ parent.children.push(span);
132
+ stack.push(span);
133
+ }
134
+ }
135
+
136
+ return root;
137
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,185 @@
1
+ import { existsSync } from "node:fs";
2
+ import { stat } from "node:fs/promises";
3
+ import {
4
+ event_checktypes__checkCrossProductUnion_DepthLimit,
5
+ event_checktypes__checkTypeRelatedTo_DepthLimit,
6
+ event_checktypes__getTypeAtFlowNode_DepthLimit,
7
+ event_checktypes__instantiateType_DepthLimit,
8
+ event_checktypes__recursiveTypeRelatedTo_DepthLimit,
9
+ event_checktypes__removeSubtypes_DepthLimit,
10
+ event_checktypes__traceUnionsOrIntersectionsTooLarge_DepthLimit,
11
+ event_checktypes__typeRelatedToDiscriminatedType_DepthLimit,
12
+ resolvedType,
13
+ traceEvent,
14
+ } from "@typeslayer/validate";
15
+ import { z } from "zod/v4";
16
+
17
+ export const absolutePath = z.string().refine(
18
+ path => {
19
+ return (
20
+ path.startsWith("/") || path.startsWith("C:\\") || path.startsWith("D:\\")
21
+ );
22
+ },
23
+ {
24
+ message: "Path must be absolute",
25
+ },
26
+ );
27
+ export type AbsolutePath = z.infer<typeof absolutePath>;
28
+
29
+ export const project = z.object({
30
+ configFilePath: absolutePath.optional(),
31
+ tracePath: absolutePath,
32
+ typesPath: absolutePath,
33
+ });
34
+ export type Project = z.infer<typeof project>;
35
+
36
+ export const projectResult = z.object({
37
+ project: project,
38
+ stdout: z.string(),
39
+ stderr: z.string(),
40
+ exitCode: z.number().optional(),
41
+ signal: z.enum(["SIGINT", "SIGTERM"]).optional(),
42
+ });
43
+ export type ProjectResult = z.infer<typeof projectResult>;
44
+
45
+ export const hotType = z.object({
46
+ resolvedType: resolvedType,
47
+ get children() {
48
+ return z.array(hotType);
49
+ },
50
+ });
51
+ export type HotType = z.infer<typeof hotType>;
52
+
53
+ export const hotSpot = z.object({
54
+ description: z.string(),
55
+ timeMs: z.number(),
56
+ get children() {
57
+ return z.array(hotSpot);
58
+ },
59
+
60
+ path: absolutePath.optional(),
61
+ types: z.array(hotType).optional(),
62
+ startLine: z.number().optional(),
63
+ startChar: z.number().optional(),
64
+ startOffset: z.number().optional(),
65
+ endLine: z.number().optional(),
66
+ endChar: z.number().optional(),
67
+ endOffset: z.number().optional(),
68
+ });
69
+ export type HotSpot = z.infer<typeof hotSpot>;
70
+
71
+ export const duplicatedPackageInstance = z.object({
72
+ path: absolutePath,
73
+ version: z.string(),
74
+ });
75
+ export type DuplicatedPackageInstance = z.infer<
76
+ typeof duplicatedPackageInstance
77
+ >;
78
+
79
+ export const duplicatedPackage = z.object({
80
+ name: z.string(),
81
+ instances: z.array(duplicatedPackageInstance),
82
+ });
83
+ export type DuplicatedPackage = z.infer<typeof duplicatedPackage>;
84
+
85
+ export const rootSpan = z.object({
86
+ name: z.literal("root"),
87
+ cat: z.literal("program"),
88
+ });
89
+ export type RootSpan = z.infer<typeof rootSpan>;
90
+
91
+ export const eventSpan = z.object({
92
+ event: z.union([traceEvent, rootSpan]),
93
+ start: z.number(),
94
+ end: z.number(),
95
+ duration: z.number(),
96
+ get children() {
97
+ return z.array(eventSpan);
98
+ },
99
+ });
100
+ export type EventSpan = z.infer<typeof eventSpan>;
101
+
102
+ export const microseconds = z.number();
103
+ export type Microseconds = z.infer<typeof microseconds>;
104
+
105
+ export const packageName = z.string();
106
+
107
+ export const packagePath = z.string();
108
+
109
+ export const nodeModulePaths = z.record(packageName, z.array(packagePath));
110
+ /** This is a map where the key corresponds to an NPM package and the value is an array of all files in that package that were used */
111
+ export type NodeModulePaths = z.infer<typeof nodeModulePaths>;
112
+
113
+ export const parseResult = z.object({
114
+ firstSpanStart: z.number(),
115
+ lastSpanEnd: z.number(),
116
+ spans: z.array(eventSpan),
117
+ unclosedStack: z.array(traceEvent),
118
+ });
119
+ export type ParseResult = z.infer<typeof parseResult>;
120
+
121
+ export const analyzeTraceOptions = z.object({
122
+ /** Events of at least this duration (in milliseconds) will reported unconditionally */
123
+ forceMillis: z.number(),
124
+ /** Events of less than this duration (in milliseconds) will suppressed unconditionally */
125
+ skipMillis: z.number(),
126
+ /** Expand types when printing */
127
+ expandTypes: z.boolean(),
128
+ /** force showing spans that are some percentage of their parent, independent of parent time */
129
+ minSpanParentPercentage: z.number(),
130
+ /** the minimum number of emitted imports from a declaration file or bundle */
131
+ importExpressionThreshold: z.number(),
132
+ });
133
+ export type AnalyzeTraceOptions = z.infer<typeof analyzeTraceOptions>;
134
+
135
+ export const isFile = async (path: string) => {
136
+ return stat(path)
137
+ .then(stats => stats.isFile())
138
+ .catch(_ => false);
139
+ };
140
+
141
+ export const throwIfNotDirectory = async (path: string) => {
142
+ if (!existsSync(path) || !(await stat(path))?.isDirectory()) {
143
+ throw new Error(`${path} is not a directory`);
144
+ }
145
+ return path;
146
+ };
147
+
148
+ export const analyzeTraceResult = z.object({
149
+ /** Events that were not closed */
150
+ unterminatedEvents: z.array(traceEvent),
151
+ /** Hot spots in the trace */
152
+ hotSpots: z.array(hotSpot),
153
+ /** Packages that are duplicated in the trace */
154
+ duplicatePackages: z.array(duplicatedPackage),
155
+ /** Paths to all node modules used in the trace */
156
+ nodeModulePaths: nodeModulePaths,
157
+ /** Depth limit events grouped by their event name */
158
+ depthLimits: z.object({
159
+ checkCrossProductUnion_DepthLimit: z.array(
160
+ event_checktypes__checkCrossProductUnion_DepthLimit,
161
+ ),
162
+ checkTypeRelatedTo_DepthLimit: z.array(
163
+ event_checktypes__checkTypeRelatedTo_DepthLimit,
164
+ ),
165
+ getTypeAtFlowNode_DepthLimit: z.array(
166
+ event_checktypes__getTypeAtFlowNode_DepthLimit,
167
+ ),
168
+ instantiateType_DepthLimit: z.array(
169
+ event_checktypes__instantiateType_DepthLimit,
170
+ ),
171
+ recursiveTypeRelatedTo_DepthLimit: z.array(
172
+ event_checktypes__recursiveTypeRelatedTo_DepthLimit,
173
+ ),
174
+ removeSubtypes_DepthLimit: z.array(
175
+ event_checktypes__removeSubtypes_DepthLimit,
176
+ ),
177
+ traceUnionsOrIntersectionsTooLarge_DepthLimit: z.array(
178
+ event_checktypes__traceUnionsOrIntersectionsTooLarge_DepthLimit,
179
+ ),
180
+ typeRelatedToDiscriminatedType_DepthLimit: z.array(
181
+ event_checktypes__typeRelatedToDiscriminatedType_DepthLimit,
182
+ ),
183
+ }),
184
+ });
185
+ export type AnalyzeTraceResult = z.infer<typeof analyzeTraceResult>;
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "dist",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "target": "ES2024",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "resolveJsonModule": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["**/*.ts"]
13
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm"],
6
+ target: "es2024",
7
+ dts: true,
8
+ splitting: true,
9
+ sourcemap: true,
10
+ clean: true,
11
+ });