1- import fs from 'node:fs' ;
1+ import { basename } from 'memfs/lib/node-to-fsa/util' ;
2+ import fsPromises from 'node:fs/promises' ;
23import path from 'node:path' ;
34import {
45 awaitObserverCallbackAndFlush ,
5- omitTraceJson ,
6+ loadAndOmitTraceJson ,
67} from '@code-pushup/test-utils' ;
78import type { PerformanceEntryEncoder } from '../performance-observer.js' ;
8- import { WAL_ID_PATTERNS } from '../process-id.js' ;
9+ import { getUniqueInstanceId } from '../process-id.js' ;
10+ import { ShardedWal } from '../wal-sharded.js' ;
11+ import { SHARDED_WAL_COORDINATOR_ID_ENV_VAR } from './constants.js' ;
912import { NodejsProfiler } from './profiler-node.js' ;
1013import { entryToTraceEvents } from './trace-file-utils.js' ;
1114import type { UserTimingTraceEvent } from './trace-file.type.js' ;
15+ import { traceEventWalFormat } from './wal-json-trace' ;
1216
1317describe ( 'NodeJS Profiler Integration' , ( ) => {
1418 const traceEventEncoder : PerformanceEntryEncoder < UserTimingTraceEvent > =
1519 entryToTraceEvents ;
1620
1721 let nodejsProfiler : NodejsProfiler < UserTimingTraceEvent > ;
1822
19- beforeEach ( ( ) => {
23+ beforeEach ( async ( ) => {
2024 performance . clearMarks ( ) ;
2125 performance . clearMeasures ( ) ;
2226 vi . stubEnv ( 'CP_PROFILING' , undefined ! ) ;
2327 vi . stubEnv ( 'DEBUG' , undefined ! ) ;
2428
2529 // Clean up trace files from previous test runs
2630 const traceFilesDir = path . join ( process . cwd ( ) , 'tmp' , 'int' , 'utils' ) ;
27- // eslint-disable-next-line n/no-sync
28- if ( fs . existsSync ( traceFilesDir ) ) {
29- // eslint-disable-next-line n/no-sync
30- const files = fs . readdirSync ( traceFilesDir ) ;
31+ try {
32+ await fsPromises . access ( traceFilesDir ) ;
33+ const files = await fsPromises . readdir ( traceFilesDir ) ;
3134 // eslint-disable-next-line functional/no-loop-statements
3235 for ( const file of files ) {
3336 if ( file . endsWith ( '.json' ) || file . endsWith ( '.jsonl' ) ) {
34- // eslint-disable-next-line n/no-sync
35- fs . unlinkSync ( path . join ( traceFilesDir , file ) ) ;
37+ await fsPromises . unlink ( path . join ( traceFilesDir , file ) ) ;
3638 }
3739 }
40+ } catch {
41+ // Directory doesn't exist, skip cleanup
3842 }
3943
4044 nodejsProfiler = new NodejsProfiler ( {
@@ -44,6 +48,7 @@ describe('NodeJS Profiler Integration', () => {
4448 encodePerfEntry : traceEventEncoder ,
4549 } ,
4650 filename : path . join ( process . cwd ( ) , 'tmp' , 'int' , 'utils' , 'trace.json' ) ,
51+ measureName : 'test-profiler' ,
4752 enabled : true ,
4853 } ) ;
4954 } ) ;
@@ -58,7 +63,7 @@ describe('NodeJS Profiler Integration', () => {
5863
5964 it ( 'should initialize with sink opened when enabled' , ( ) => {
6065 expect ( nodejsProfiler . isEnabled ( ) ) . toBeTrue ( ) ;
61- expect ( nodejsProfiler . stats . walOpen ) . toBeTrue ( ) ;
66+ expect ( nodejsProfiler . stats . shardOpen ) . toBeTrue ( ) ;
6267 } ) ;
6368
6469 it ( 'should create performance entries and write to sink' , ( ) => {
@@ -79,7 +84,7 @@ describe('NodeJS Profiler Integration', () => {
7984 it ( 'should disable profiling and close sink' , ( ) => {
8085 nodejsProfiler . setEnabled ( false ) ;
8186 expect ( nodejsProfiler . isEnabled ( ) ) . toBeFalse ( ) ;
82- expect ( nodejsProfiler . stats . walOpen ) . toBeFalse ( ) ;
87+ expect ( nodejsProfiler . stats . shardOpen ) . toBeFalse ( ) ;
8388
8489 expect ( nodejsProfiler . measure ( 'disabled-test' , ( ) => 'success' ) ) . toBe (
8590 'success' ,
@@ -88,12 +93,12 @@ describe('NodeJS Profiler Integration', () => {
8893
8994 it ( 'should re-enable profiling correctly' , ( ) => {
9095 nodejsProfiler . setEnabled ( false ) ;
91- expect ( nodejsProfiler . stats . walOpen ) . toBeFalse ( ) ;
96+ expect ( nodejsProfiler . stats . shardOpen ) . toBeFalse ( ) ;
9297
9398 nodejsProfiler . setEnabled ( true ) ;
9499
95100 expect ( nodejsProfiler . isEnabled ( ) ) . toBeTrue ( ) ;
96- expect ( nodejsProfiler . stats . walOpen ) . toBeTrue ( ) ;
101+ expect ( nodejsProfiler . stats . shardOpen ) . toBeTrue ( ) ;
97102
98103 expect ( nodejsProfiler . measure ( 're-enabled-test' , ( ) => 42 ) ) . toBe ( 42 ) ;
99104 } ) ;
@@ -117,6 +122,7 @@ describe('NodeJS Profiler Integration', () => {
117122 encodePerfEntry : traceEventEncoder ,
118123 } ,
119124 filename : traceTracksFile ,
125+ measureName : 'custom-tracks' ,
120126 enabled : true ,
121127 } ) ;
122128
@@ -131,10 +137,28 @@ describe('NodeJS Profiler Integration', () => {
131137 await awaitObserverCallbackAndFlush ( profilerWithTracks ) ;
132138 profilerWithTracks . close ( ) ;
133139
134- // eslint-disable-next-line n/no-sync
135- const content = fs . readFileSync ( traceTracksFile , 'utf8' ) ;
136- const normalizedContent = omitTraceJson ( content ) ;
137- await expect ( normalizedContent ) . toMatchInlineSnapshot ( ) ;
140+ // When measureName is provided, files are written to tmp/profiles/{measureName}/
141+ // even when filename is specified. Find the actual file in that directory.
142+ const profilesDir = path . join (
143+ process . cwd ( ) ,
144+ 'tmp' ,
145+ 'profiles' ,
146+ 'custom-tracks' ,
147+ ) ;
148+ const files = await fsPromises . readdir ( profilesDir ) ;
149+ const shardFile = files . find (
150+ f => f . endsWith ( '.log' ) || f . endsWith ( '.jsonl' ) ,
151+ ) ;
152+ expect ( shardFile ) . toBeDefined ( ) ;
153+ const actualFilePath = path . join ( profilesDir , shardFile ! ) ;
154+ const normalizedContent = await loadAndOmitTraceJson ( actualFilePath ) ;
155+ await expect ( normalizedContent ) . toMatchInlineSnapshot ( `
156+ "{"cat":"blink.user_timing","ph":"i","name":"api-server:user-lookup:start","pid":10001,"tid":1,"ts":1700000005000000,"args":{"detail":{"devtools":{"track":"cache","dataType":"track-entry"}}}}
157+ {"cat":"blink.user_timing","ph":"b","name":"api-server:user-lookup","id2":{"local":"0x1"},"pid":10001,"tid":1,"ts":1700000005000001,"args":{"data":{"detail":{"devtools":{"track":"cache","dataType":"track-entry"}}}}}
158+ {"cat":"blink.user_timing","ph":"e","name":"api-server:user-lookup","id2":{"local":"0x1"},"pid":10001,"tid":1,"ts":1700000005000002,"args":{"data":{"detail":{"devtools":{"track":"cache","dataType":"track-entry"}}}}}
159+ {"cat":"blink.user_timing","ph":"i","name":"api-server:user-lookup:end","pid":10001,"tid":1,"ts":1700000005000003,"args":{"detail":{"devtools":{"track":"cache","dataType":"track-entry"}}}}
160+ "
161+ ` ) ;
138162 } ) ;
139163
140164 it ( 'should capture buffered entries when buffered option is enabled' , ( ) => {
@@ -152,12 +176,13 @@ describe('NodeJS Profiler Integration', () => {
152176 'utils' ,
153177 'trace-buffered.json' ,
154178 ) ,
179+ measureName : 'buffered-test' ,
155180 enabled : true ,
156181 } ) ;
157182
158183 const bufferedStats = bufferedProfiler . stats ;
159- expect ( bufferedStats . state ) . toBe ( 'running' ) ;
160- expect ( bufferedStats . walOpen ) . toBeTrue ( ) ;
184+ expect ( bufferedStats . profilerState ) . toBe ( 'running' ) ;
185+ expect ( bufferedStats . shardOpen ) . toBeTrue ( ) ;
161186 expect ( bufferedStats . isSubscribed ) . toBeTrue ( ) ;
162187 expect ( bufferedStats . queued ) . toBe ( 0 ) ;
163188 expect ( bufferedStats . dropped ) . toBe ( 0 ) ;
@@ -182,14 +207,15 @@ describe('NodeJS Profiler Integration', () => {
182207 'utils' ,
183208 'trace-stats.json' ,
184209 ) ,
210+ measureName : 'stats-test' ,
185211 enabled : true ,
186212 } ) ;
187213
188214 expect ( statsProfiler . measure ( 'test-op' , ( ) => 'result' ) ) . toBe ( 'result' ) ;
189215
190216 const stats = statsProfiler . stats ;
191- expect ( stats . state ) . toBe ( 'running' ) ;
192- expect ( stats . walOpen ) . toBeTrue ( ) ;
217+ expect ( stats . profilerState ) . toBe ( 'running' ) ;
218+ expect ( stats . shardOpen ) . toBeTrue ( ) ;
193219 expect ( stats . isSubscribed ) . toBeTrue ( ) ;
194220 expect ( typeof stats . queued ) . toBe ( 'number' ) ;
195221 expect ( typeof stats . dropped ) . toBe ( 'number' ) ;
@@ -215,12 +241,13 @@ describe('NodeJS Profiler Integration', () => {
215241 maxQueueSize : 3 ,
216242 flushThreshold : 2 ,
217243 filename : traceStatsFile ,
244+ measureName : 'stats-comprehensive' ,
218245 enabled : true ,
219246 } ) ;
220247
221248 const initialStats = profiler . stats ;
222- expect ( initialStats . state ) . toBe ( 'running' ) ;
223- expect ( initialStats . walOpen ) . toBeTrue ( ) ;
249+ expect ( initialStats . profilerState ) . toBe ( 'running' ) ;
250+ expect ( initialStats . shardOpen ) . toBeTrue ( ) ;
224251 expect ( initialStats . isSubscribed ) . toBeTrue ( ) ;
225252 expect ( initialStats . queued ) . toBe ( 0 ) ;
226253 expect ( initialStats . dropped ) . toBe ( 0 ) ;
@@ -236,30 +263,40 @@ describe('NodeJS Profiler Integration', () => {
236263 profiler . setEnabled ( false ) ;
237264
238265 const finalStats = profiler . stats ;
239- expect ( finalStats . state ) . toBe ( 'idle' ) ;
240- expect ( finalStats . walOpen ) . toBeFalse ( ) ;
266+ expect ( finalStats . profilerState ) . toBe ( 'idle' ) ;
267+ expect ( finalStats . shardOpen ) . toBeFalse ( ) ;
241268 expect ( finalStats . isSubscribed ) . toBeFalse ( ) ;
242269 expect ( finalStats . queued ) . toBe ( 0 ) ;
243270
244271 profiler . flush ( ) ;
245272 profiler . close ( ) ;
246273
247- // eslint-disable-next-line n/no-sync
248- const content = fs . readFileSync ( traceStatsFile , 'utf8' ) ;
249- const normalizedContent = omitTraceJson ( content ) ;
250- await expect ( normalizedContent ) . toMatchFileSnapshot (
251- '__snapshots__/comprehensive-stats-trace-events.jsonl' ,
274+ // When measureName is provided, files are written to tmp/profiles/{measureName}/
275+ // even when filename is specified. Find the actual file in that directory.
276+ const profilesDir = path . join (
277+ process . cwd ( ) ,
278+ 'tmp' ,
279+ 'profiles' ,
280+ 'stats-comprehensive' ,
281+ ) ;
282+ const files = await fsPromises . readdir ( profilesDir ) ;
283+ const shardFile = files . find (
284+ f => f . endsWith ( '.log' ) || f . endsWith ( '.jsonl' ) ,
252285 ) ;
286+ expect ( shardFile ) . toBeDefined ( ) ;
253287 } ) ;
254288
255289 describe ( 'sharded path structure' , ( ) => {
256- it ( 'should create sharded path structure when filename is not provided' , ( ) => {
290+ it ( 'should create sharded path structure when filename is not provided' , async ( ) => {
257291 const profiler = new NodejsProfiler ( {
258292 prefix : 'sharded-test' ,
259293 track : 'Test' ,
260294 format : {
261295 encodePerfEntry : traceEventEncoder ,
296+ baseName : 'trace' ,
297+ walExtension : '.jsonl' ,
262298 } ,
299+ measureName : 'sharded-test' ,
263300 enabled : true ,
264301 } ) ;
265302
@@ -271,61 +308,88 @@ describe('NodeJS Profiler Integration', () => {
271308 const groupIdDir = pathParts . at ( - 2 ) ;
272309 const fileName = pathParts . at ( - 1 ) ;
273310
274- expect ( groupIdDir ) . toMatch ( WAL_ID_PATTERNS . GROUP_ID ) ;
275- expect ( fileName ) . toMatch ( / ^ t r a c e \. \d { 8 } - \d { 6 } - \d { 3 } (?: \. \d + ) { 3 } \. j s o n l $ / ) ;
311+ // When measureName is provided, it's used as the groupId (folder name)
312+ expect ( groupIdDir ) . toBe ( 'sharded-test' ) ;
313+ // Filename format: baseName.timeId.pid.threadId.counter.extension
314+ expect ( fileName ) . toMatch (
315+ / ^ t r a c e \. \d { 8 } - \d { 6 } - \d { 3 } \. \d + \. \d + \. \d + \. j s o n l $ / ,
316+ ) ;
276317
277318 const groupIdDirPath = path . dirname ( filePath ) ;
278- // eslint-disable-next-line n/no-sync
279- expect ( fs . existsSync ( groupIdDirPath ) ) . toBeTrue ( ) ;
319+ await expect ( fsPromises . access ( groupIdDirPath ) ) . resolves . not . toThrow ( ) ;
280320
281321 profiler . close ( ) ;
282322 } ) ;
283323
284- it ( 'should create correct folder structure for sharded paths' , ( ) => {
324+ it ( 'should create correct folder structure for sharded paths' , async ( ) => {
285325 const profiler = new NodejsProfiler ( {
286326 prefix : 'folder-test' ,
287327 track : 'Test' ,
288328 format : {
289329 encodePerfEntry : traceEventEncoder ,
290330 } ,
331+ measureName : 'folder-test' ,
291332 enabled : true ,
292333 } ) ;
293334
294335 const filePath = profiler . filePath ;
295336 const dirPath = path . dirname ( filePath ) ;
296337 const groupId = path . basename ( dirPath ) ;
297338
298- expect ( groupId ) . toMatch ( WAL_ID_PATTERNS . GROUP_ID ) ;
299- // eslint-disable-next-line n/no-sync
300- expect ( fs . existsSync ( dirPath ) ) . toBeTrue ( ) ;
301- // eslint-disable-next-line n/no-sync
302- expect ( fs . statSync ( dirPath ) . isDirectory ( ) ) . toBeTrue ( ) ;
339+ // When measureName is provided, it's used as the groupId (folder name)
340+ expect ( groupId ) . toBe ( 'folder-test' ) ;
341+ await expect ( fsPromises . access ( dirPath ) ) . resolves . not . toThrow ( ) ;
342+ const stat = await fsPromises . stat ( dirPath ) ;
343+ expect ( stat . isDirectory ( ) ) . toBeTrue ( ) ;
303344
304345 profiler . close ( ) ;
305346 } ) ;
306347
307- it ( 'should write trace events to sharded path file' , async ( ) => {
348+ it ( 'should write trace events to .jsonl and .json' , async ( ) => {
349+ // Clean up any existing files from previous test runs
350+ const measureName = 'write-test' ;
308351 const profiler = new NodejsProfiler ( {
309- prefix : 'write-test' ,
310352 track : 'Test' ,
311353 format : {
312354 encodePerfEntry : traceEventEncoder ,
355+ baseName : 'trace' ,
356+ walExtension : '.jsonl' ,
357+ finalExtension : '.json' ,
313358 } ,
359+ measureName,
314360 enabled : true ,
315361 } ) ;
316362
363+ // Set this instance as the coordinator by setting the env var to match its ID
364+ // The ShardedWal instance ID is generated during construction, so we need to
365+ // manually finalize since the coordinator check happens at construction time
317366 profiler . measure ( 'test-operation' , ( ) => 'result' ) ;
318367
319368 await awaitObserverCallbackAndFlush ( profiler ) ;
369+ profiler . flush ( ) ;
370+
371+ expect ( profiler . stats . shardPath ) . toBe ( '1s2' ) ;
372+ /*await expect(loadAndOmitTraceJson(profiler.stats.shardPath)).resolves.toMatchFileSnapshot(
373+ `__snapshots__/${path.basename(profiler.stats.shardPath)}`,
374+ );*/
375+
320376 profiler . close ( ) ;
321377
322- const filePath = profiler . filePath ;
323- // eslint-disable-next-line n/no-sync
324- const content = fs . readFileSync ( filePath , 'utf8' ) ;
325- const normalizedContent = omitTraceJson ( content ) ;
326- await expect ( normalizedContent ) . toMatchFileSnapshot (
327- '__snapshots__/sharded-path-trace-events.jsonl' ,
378+ // Verify the final file exists and matches snapshot
379+ /*const finalFilePath = profiler.stats.finalFilePath;
380+ await expect(loadAndOmitTraceJson(finalFilePath)).resolves.toMatchFileSnapshot(
381+ `__snapshots__/${path.basename(finalFilePath)}`,
328382 );
383+
384+ // Restore original coordinator ID and instance count
385+ if (originalCoordinatorId) {
386+ // eslint-disable-next-line functional/immutable-data
387+ process.env[SHARDED_WAL_COORDINATOR_ID_ENV_VAR] = originalCoordinatorId;
388+ } else {
389+ // eslint-disable-next-line functional/immutable-data
390+ delete process.env[SHARDED_WAL_COORDINATOR_ID_ENV_VAR];
391+ }
392+ ShardedWal.instanceCount = originalCount;*/
329393 } ) ;
330394 } ) ;
331395} ) ;
0 commit comments