@atlaspack/profiler 2.14.1-canary.43 → 2.14.1-canary.430

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaspack/profiler",
3
- "version": "2.14.1-canary.43+47cfa92f1",
3
+ "version": "2.14.1-canary.430+8acee7f43",
4
4
  "description": "Blazing fast, zero configuration web application bundler",
5
5
  "license": "(MIT OR Apache-2.0)",
6
6
  "publishConfig": {
@@ -10,21 +10,24 @@
10
10
  "type": "git",
11
11
  "url": "https://github.com/atlassian-labs/atlaspack.git"
12
12
  },
13
- "main": "lib/index.js",
14
- "source": "src/index.js",
13
+ "main": "./lib/index.js",
14
+ "source": "./src/index.ts",
15
+ "types": "./lib/types/index.d.ts",
15
16
  "engines": {
16
17
  "node": ">= 16.0.0"
17
18
  },
18
19
  "scripts": {
19
- "build-ts": "flow-to-ts src/*.js --write && rm -f ./src/*.d.ts && tsc --emitDeclarationOnly --declaration --esModuleInterop --target es2015 --moduleResolution node16 --module node16 src/*.ts && mkdir -p lib && mv src/*.d.ts lib/. && rm src/*.ts && node build-ts.js",
20
- "check-ts": "tsc --noEmit lib/index.d.ts"
20
+ "build:lib": "gulp build --gulpfile ../../../gulpfile.js --cwd ."
21
21
  },
22
22
  "dependencies": {
23
- "@atlaspack/diagnostic": "2.14.1-canary.43+47cfa92f1",
24
- "@atlaspack/events": "2.14.1-canary.43+47cfa92f1",
25
- "@atlaspack/types-internal": "2.14.1-canary.43+47cfa92f1",
23
+ "@atlaspack/diagnostic": "2.14.1-canary.430+8acee7f43",
24
+ "@atlaspack/events": "2.14.1-canary.430+8acee7f43",
25
+ "@atlaspack/logger": "2.14.5-canary.362+8acee7f43",
26
+ "@atlaspack/types-internal": "2.14.1-canary.430+8acee7f43",
27
+ "@atlaspack/utils": "2.14.5-canary.362+8acee7f43",
28
+ "chalk": "^4.1.2",
26
29
  "chrome-trace-event": "^1.0.2"
27
30
  },
28
31
  "type": "commonjs",
29
- "gitHead": "47cfa92f1e2dfae9abcc2db48269f3ce64f98fe3"
30
- }
32
+ "gitHead": "8acee7f433511c6e33b02ce793e01ed347ecd6d8"
33
+ }
@@ -0,0 +1,245 @@
1
+ import {getTimeId} from '@atlaspack/utils';
2
+ import logger from '@atlaspack/logger';
3
+ import readline from 'readline';
4
+ import chalk from 'chalk';
5
+ import {exec} from 'child_process';
6
+ import {promisify} from 'util';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ export type NativeProfilerType = 'instruments' | 'samply';
11
+
12
+ export default class NativeProfiler {
13
+ startProfiling(profilerType: NativeProfilerType): Promise<void> {
14
+ const pid = process.pid;
15
+ const timeId = getTimeId();
16
+
17
+ let filename: string;
18
+ let command: string;
19
+
20
+ logger.info({
21
+ origin: '@atlaspack/profiler',
22
+ message: 'Starting native profiling...',
23
+ });
24
+
25
+ if (profilerType === 'instruments') {
26
+ filename = `native-profile-${timeId}.trace`;
27
+ command = `xcrun xctrace record --template "CPU Profiler" --output ${filename} --attach ${pid}`;
28
+ } else {
29
+ filename = `native-profile-${timeId}.json`;
30
+ command = `samply record --save-only --output ${filename} --pid ${pid}`;
31
+ }
32
+
33
+ // Display banner with PID and command
34
+ // Strip ANSI codes for length calculation
35
+ // eslint-disable-next-line no-control-regex
36
+ const stripAnsi = (str: string) => str.replace(/\u001b\[[0-9;]*m/g, '');
37
+ const boxWidth = Math.max(60, stripAnsi(command).length + 6);
38
+ const title = 'Native Profiling';
39
+ const titlePadding = Math.floor((boxWidth - title.length - 2) / 2);
40
+ const isTTY = process.stdin.isTTY;
41
+ const maxWaitTime = 30; // seconds
42
+
43
+ const padLine = (content: string) => {
44
+ const contentLength = stripAnsi(content).length;
45
+ const padding = Math.max(0, boxWidth - contentLength - 2);
46
+ return (
47
+ chalk.blue('│') +
48
+ ' ' +
49
+ content +
50
+ ' '.repeat(padding) +
51
+ ' ' +
52
+ chalk.blue('│')
53
+ );
54
+ };
55
+
56
+ // Make the command visually distinct and easy to copy
57
+ // Note: Hyperlinks can cause issues with commands (words become separate links)
58
+ // So we just make it visually prominent with colors
59
+ const makeCommandDisplay = (cmd: string) => {
60
+ return chalk.cyan.bold(cmd);
61
+ };
62
+
63
+ // Contextual message based on TTY
64
+ const continueMessage = isTTY
65
+ ? 'Press Enter or start the profiler to continue'
66
+ : `Build will continue when profiler has started, or after ${maxWaitTime}s`;
67
+
68
+ const banner = [
69
+ '',
70
+ chalk.blue('┌' + '─'.repeat(boxWidth) + '┐'),
71
+ chalk.blue('│') +
72
+ ' '.repeat(titlePadding) +
73
+ chalk.blue.bold(title) +
74
+ ' '.repeat(boxWidth - title.length - titlePadding) +
75
+ chalk.blue('│'),
76
+ chalk.blue('├' + '─'.repeat(boxWidth) + '┤'),
77
+ padLine(`${chalk.gray('PID:')} ${chalk.white.bold(String(pid))}`),
78
+ padLine(''),
79
+ padLine(chalk.gray('Command:')),
80
+ padLine(makeCommandDisplay(command)),
81
+ padLine(''),
82
+ padLine(chalk.gray('Run the command above to start profiling.')),
83
+ padLine(chalk.gray(continueMessage)),
84
+ chalk.blue('└' + '─'.repeat(boxWidth) + '┘'),
85
+ '',
86
+ ].join('\n');
87
+
88
+ // eslint-disable-next-line no-console
89
+ console.log(banner);
90
+
91
+ // In both interactive and non-interactive mode, detect when profiler is running
92
+ // In interactive mode, also allow user to press Enter to continue
93
+ if (!process.stdin.isTTY) {
94
+ return this.waitForProfiler(profilerType, pid);
95
+ }
96
+
97
+ // Interactive mode: wait for either user to press Enter OR profiler to be detected
98
+ return new Promise<void>((resolve) => {
99
+ let resolved = false;
100
+ const doResolve = () => {
101
+ if (resolved) return;
102
+ resolved = true;
103
+ logger.info({
104
+ origin: '@atlaspack/profiler',
105
+ message: 'Native profiling setup complete',
106
+ });
107
+ resolve();
108
+ };
109
+
110
+ const rl = readline.createInterface({
111
+ input: process.stdin,
112
+ output: process.stdout,
113
+ });
114
+
115
+ // User presses Enter
116
+ rl.on('line', () => {
117
+ rl.close();
118
+ doResolve();
119
+ });
120
+
121
+ // Also poll for profiler in the background
122
+ this.pollForProfiler(profilerType, pid, doResolve);
123
+ });
124
+ }
125
+
126
+ private waitForProfiler(
127
+ profilerType: NativeProfilerType,
128
+ pid: number,
129
+ ): Promise<void> {
130
+ logger.info({
131
+ origin: '@atlaspack/profiler',
132
+ message: 'Non-interactive mode: waiting for profiler to attach...',
133
+ });
134
+
135
+ return new Promise<void>((resolve) => {
136
+ this.pollForProfiler(profilerType, pid, () => {
137
+ logger.info({
138
+ origin: '@atlaspack/profiler',
139
+ message: 'Native profiling setup complete',
140
+ });
141
+ resolve();
142
+ });
143
+ });
144
+ }
145
+
146
+ private async pollForProfiler(
147
+ profilerType: NativeProfilerType,
148
+ pid: number,
149
+ onDetected: () => void,
150
+ ): Promise<void> {
151
+ const maxAttempts = 60; // 60 attempts * 500ms = 30 seconds max
152
+ const pollInterval = 500; // 500ms between checks
153
+
154
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
155
+ const isRunning = await this.checkProfilerRunning(profilerType, pid);
156
+
157
+ if (isRunning) {
158
+ // Instruments takes longer to start up (~5s), samply needs ~1s
159
+ const waitTime = profilerType === 'instruments' ? 5000 : 1000;
160
+ logger.info({
161
+ origin: '@atlaspack/profiler',
162
+ message: `Profiler detected, waiting ${waitTime}ms before continuing...`,
163
+ });
164
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
165
+ onDetected();
166
+ return;
167
+ }
168
+
169
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
170
+ }
171
+
172
+ // If we couldn't detect the profiler after 30 seconds, log a warning and continue anyway
173
+ logger.warn({
174
+ origin: '@atlaspack/profiler',
175
+ message:
176
+ 'Could not detect profiler after 30 seconds, continuing anyway...',
177
+ });
178
+ onDetected();
179
+ }
180
+
181
+ private async checkProfilerRunning(
182
+ profilerType: NativeProfilerType,
183
+ pid: number,
184
+ ): Promise<boolean> {
185
+ try {
186
+ // Get all processes and filter in JavaScript
187
+ const {stdout} = await execAsync('ps aux');
188
+ const lines = stdout.split('\n').filter((line) => line.trim().length > 0);
189
+
190
+ // Use word boundaries to match the PID as a complete number
191
+ const pidRegex = new RegExp(`\\b${pid}\\b`);
192
+
193
+ // Determine the profiler process name to look for
194
+ const profilerName =
195
+ profilerType === 'instruments' ? 'xctrace' : 'samply';
196
+
197
+ for (const line of lines) {
198
+ const lowerLine = line.toLowerCase();
199
+
200
+ // Skip lines that are part of our own process checking (avoid false positives)
201
+ // Skip lines containing "ps aux" or "grep" to avoid matching our own commands
202
+ if (lowerLine.includes('ps aux') || lowerLine.includes(' grep ')) {
203
+ continue;
204
+ }
205
+
206
+ // Skip our own process (the Atlaspack process itself)
207
+ // The PID column is the second field in ps aux output
208
+ const fields = line.trim().split(/\s+/);
209
+ if (fields.length >= 2 && fields[1] === String(pid)) {
210
+ continue;
211
+ }
212
+
213
+ // Check if this line contains the profiler name as a command
214
+ const profilerRegex = new RegExp(`\\b${profilerName}\\b`);
215
+ if (!profilerRegex.test(lowerLine)) {
216
+ continue;
217
+ }
218
+
219
+ // Now check if our PID appears in the command arguments (not in the PID column)
220
+ // The PID should appear after the profiler command, typically as --pid <pid> or --attach <pid>
221
+ // We need to check the command portion, which starts around column 11 in ps aux
222
+ // For safety, check if PID appears after the profiler name in the line
223
+ const profilerIndex = lowerLine.indexOf(profilerName);
224
+ if (profilerIndex === -1) {
225
+ continue;
226
+ }
227
+
228
+ // Check if PID appears in the command portion (after the profiler name)
229
+ const commandPortion = line.substring(profilerIndex);
230
+ if (pidRegex.test(commandPortion)) {
231
+ return true;
232
+ }
233
+ }
234
+
235
+ return false;
236
+ } catch (error: any) {
237
+ // If the command fails, log and return false
238
+ logger.warn({
239
+ origin: '@atlaspack/profiler',
240
+ message: `Error checking profiler status: ${error.message}`,
241
+ });
242
+ return false;
243
+ }
244
+ }
245
+ }
@@ -1,50 +1,50 @@
1
- // @flow
2
1
  import type {Session} from 'inspector';
3
2
  import invariant from 'assert';
4
3
  import ThrowableDiagnostic from '@atlaspack/diagnostic';
5
4
 
6
5
  // https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
7
- export type Profile = {|
8
- nodes: Array<ProfileNode>,
9
- startTime: number,
10
- endTime: number,
11
- samples?: Array<number>,
12
- timeDeltas?: Array<number>,
13
- |};
6
+ export type Profile = {
7
+ nodes: Array<ProfileNode>;
8
+ startTime: number;
9
+ endTime: number;
10
+ samples?: Array<number>;
11
+ timeDeltas?: Array<number>;
12
+ };
14
13
 
15
14
  // https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ProfileNode
16
- type ProfileNode = {|
17
- id: number,
18
- callFrame: CallFrame,
19
- hitCount?: number,
20
- children?: Array<number>,
21
- deoptReason?: string,
22
- positionTicks?: PositionTickInfo,
23
- |};
15
+ type ProfileNode = {
16
+ id: number;
17
+ callFrame: CallFrame;
18
+ hitCount?: number;
19
+ children?: Array<number>;
20
+ deoptReason?: string;
21
+ positionTicks?: PositionTickInfo;
22
+ };
24
23
 
25
24
  // https://chromedevtools.github.io/devtools-protocol/tot/Runtime#type-CallFrame
26
- type CallFrame = {|
27
- functionName: string,
28
- scriptId: string,
29
- url: string,
30
- lineNumber: string,
31
- columnNumber: string,
32
- |};
25
+ type CallFrame = {
26
+ functionName: string;
27
+ scriptId: string;
28
+ url: string;
29
+ lineNumber: string;
30
+ columnNumber: string;
31
+ };
33
32
 
34
33
  // https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-PositionTickInfo
35
- type PositionTickInfo = {|
36
- line: number,
37
- ticks: number,
38
- |};
34
+ type PositionTickInfo = {
35
+ line: number;
36
+ ticks: number;
37
+ };
39
38
 
40
39
  export default class SamplingProfiler {
40
+ // @ts-expect-error not initialized
41
41
  session: Session;
42
42
 
43
- startProfiling(): Promise<mixed> {
43
+ startProfiling(): Promise<unknown> {
44
44
  let inspector;
45
45
  try {
46
46
  inspector = require('inspector');
47
- } catch (err) {
47
+ } catch (err: any) {
48
48
  throw new ThrowableDiagnostic({
49
49
  diagnostic: {
50
50
  message: `The inspector module isn't available`,
@@ -68,18 +68,37 @@ export default class SamplingProfiler {
68
68
 
69
69
  sendCommand(
70
70
  method: string,
71
- params?: mixed,
72
- ): Promise<{profile: Profile, ...}> {
71
+ params?: any,
72
+ ): Promise<{
73
+ profile: Profile;
74
+ }> {
73
75
  invariant(this.session != null);
74
- return new Promise((resolve, reject) => {
75
- this.session.post(method, params, (err, p) => {
76
- if (err == null) {
77
- resolve((p: {profile: Profile, ...}));
78
- } else {
79
- reject(err);
80
- }
81
- });
82
- });
76
+ return new Promise(
77
+ (
78
+ resolve: (
79
+ result:
80
+ | Promise<{
81
+ profile: Profile;
82
+ }>
83
+ | {
84
+ profile: Profile;
85
+ },
86
+ ) => void,
87
+ reject: (error?: any) => void,
88
+ ) => {
89
+ this.session.post(method, params, (err, p) => {
90
+ if (err == null) {
91
+ resolve(
92
+ p as {
93
+ profile: Profile;
94
+ },
95
+ );
96
+ } else {
97
+ reject(err);
98
+ }
99
+ });
100
+ },
101
+ );
83
102
  }
84
103
 
85
104
  destroy() {
@@ -1,4 +1,3 @@
1
- // @flow
2
1
  import type {Profile} from './SamplingProfiler';
3
2
  import type {Writable} from 'stream';
4
3
  import {Tracer} from 'chrome-trace-event';
@@ -1,19 +1,17 @@
1
- // @flow strict-local
2
-
3
1
  import type {
4
2
  TraceEvent,
5
3
  IDisposable,
6
4
  PluginTracer as IPluginTracer,
7
- } from '@atlaspack/types';
5
+ } from '@atlaspack/types-internal';
8
6
  import type {
9
7
  TraceMeasurement as ITraceMeasurement,
10
8
  TraceMeasurementData,
11
9
  } from './types';
12
- // @ts-ignore
13
10
  import {ValueEmitter} from '@atlaspack/events';
14
11
 
15
12
  import {performance} from 'perf_hooks';
16
13
 
14
+ // @ts-expect-error TS7034
17
15
  let tid;
18
16
  try {
19
17
  tid = require('worker_threads').threadId;
@@ -29,9 +27,14 @@ class TraceMeasurement implements ITraceMeasurement {
29
27
  #pid: number;
30
28
  #tid: number;
31
29
  #start: number;
32
- // $FlowFixMe
33
30
  #data: any;
34
- constructor(tracer: Tracer, name, pid, tid, data) {
31
+ constructor(
32
+ tracer: Tracer,
33
+ name: string,
34
+ pid: number,
35
+ tid: number,
36
+ data: TraceMeasurementData,
37
+ ) {
35
38
  this.#name = name;
36
39
  this.#pid = pid;
37
40
  this.#tid = tid;
@@ -60,11 +63,11 @@ export default class Tracer {
60
63
 
61
64
  #enabled: boolean = false;
62
65
 
63
- onTrace(cb: (event: TraceEvent) => mixed): IDisposable {
66
+ onTrace(cb: (event: TraceEvent) => unknown): IDisposable {
64
67
  return this.#traceEmitter.addListener(cb);
65
68
  }
66
69
 
67
- async wrap(name: string, fn: () => mixed): Promise<void> {
70
+ async wrap(name: string, fn: () => unknown): Promise<void> {
68
71
  let measurement = this.createMeasurement(name);
69
72
  try {
70
73
  await fn();
@@ -77,17 +80,22 @@ export default class Tracer {
77
80
  name: string,
78
81
  category: string = 'Core',
79
82
  argumentName?: string,
80
- otherArgs?: {[key: string]: mixed},
83
+ otherArgs?: {
84
+ [key: string]: unknown;
85
+ },
81
86
  ): ITraceMeasurement | null {
82
87
  if (!this.enabled) return null;
83
88
 
84
89
  // We create `args` in a fairly verbose way to avoid object
85
90
  // allocation where not required.
86
- let args: {[key: string]: mixed};
91
+ let args: {
92
+ [key: string]: unknown;
93
+ };
87
94
  if (typeof argumentName === 'string') {
88
95
  args = {name: argumentName};
89
96
  }
90
97
  if (typeof otherArgs === 'object') {
98
+ // @ts-expect-error TS2454
91
99
  if (typeof args == 'undefined') {
92
100
  args = {};
93
101
  }
@@ -98,9 +106,11 @@ export default class Tracer {
98
106
 
99
107
  const data: TraceMeasurementData = {
100
108
  categories: [category],
109
+ // @ts-expect-error TS2454
101
110
  args,
102
111
  };
103
112
 
113
+ // @ts-expect-error TS7005
104
114
  return new TraceMeasurement(this, name, pid, tid, data);
105
115
  }
106
116
 
@@ -124,10 +134,10 @@ export default class Tracer {
124
134
 
125
135
  export const tracer: Tracer = new Tracer();
126
136
 
127
- type TracerOpts = {|
128
- origin: string,
129
- category: string,
130
- |};
137
+ type TracerOpts = {
138
+ origin: string;
139
+ category: string;
140
+ };
131
141
  export class PluginTracer implements IPluginTracer {
132
142
  /** @private */
133
143
  origin: string;
@@ -149,7 +159,9 @@ export class PluginTracer implements IPluginTracer {
149
159
  name: string,
150
160
  category?: string,
151
161
  argumentName?: string,
152
- otherArgs?: {[key: string]: mixed},
162
+ otherArgs?: {
163
+ [key: string]: unknown;
164
+ },
153
165
  ): ITraceMeasurement | null {
154
166
  return tracer.createMeasurement(
155
167
  name,
@@ -1,5 +1,6 @@
1
- // @flow
2
1
  export {default as SamplingProfiler} from './SamplingProfiler';
3
2
  export {default as Trace} from './Trace';
4
3
  export {tracer, PluginTracer} from './Tracer';
5
4
  export type {TraceMeasurement, TraceMeasurementData} from './types';
5
+ export {default as NativeProfiler} from './NativeProfiler';
6
+ export type {NativeProfilerType} from './NativeProfiler';
package/src/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ export type {TraceMeasurement} from '@atlaspack/types-internal';
2
+
3
+ export type TraceMeasurementData = {
4
+ readonly categories: string[];
5
+ readonly args?: {
6
+ [key: string]: unknown;
7
+ };
8
+ };
@@ -1,11 +1,10 @@
1
- // @flow
2
1
  import {tracer, PluginTracer} from '../src/Tracer';
3
2
  import sinon from 'sinon';
4
3
  import assert from 'assert';
5
4
 
6
5
  describe('Tracer', () => {
7
- let onTrace;
8
- let traceDisposable;
6
+ let onTrace: any;
7
+ let traceDisposable: any;
9
8
  beforeEach(() => {
10
9
  onTrace = sinon.spy();
11
10
  traceDisposable = tracer.onTrace(onTrace);
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["src"],
4
+ "compilerOptions": {
5
+ "composite": true
6
+ },
7
+ "references": [
8
+ {
9
+ "path": "../../utils/events/tsconfig.json"
10
+ },
11
+ {
12
+ "path": "../diagnostic/tsconfig.json"
13
+ },
14
+ {
15
+ "path": "../types-internal/tsconfig.json"
16
+ },
17
+ {
18
+ "path": "../logger/tsconfig.json"
19
+ },
20
+ {
21
+ "path": "../utils/tsconfig.json"
22
+ }
23
+ ]
24
+ }