@@ -373,20 +373,18 @@ async function main() {
373373 { name : "SpanExitEvent" , timestamp : 1200 , fields : { worker_id : 0 , span_id : 2 , span_name : "redis_get" , fields : { key : "foo" } } } ,
374374 { name : "SpanExitEvent" , timestamp : 1300 , fields : { worker_id : 0 , span_id : 1 , span_name : "handle_request" , fields : { user_id : "42" } } } ,
375375 ] ;
376- const { spansByWorker, spanMeta } = buildSpanData ( customEvents ) ;
377- const w0 = spansByWorker [ 0 ] || [ ] ;
378- if ( w0 . length !== 2 ) fail ( `Expected 2 span intervals on worker 0, got ${ w0 . length } ` ) ;
379- if ( w0 [ 0 ] . spanName !== "handle_request" && w0 [ 1 ] . spanName !== "handle_request" )
380- fail ( "Missing handle_request span" ) ;
381- if ( w0 [ 0 ] . spanName !== "redis_get" && w0 [ 1 ] . spanName !== "redis_get" )
382- fail ( "Missing redis_get span" ) ;
383- // Verify sorted by start time
384- if ( w0 [ 0 ] . start > w0 [ 1 ] . start ) fail ( "Spans not sorted by start time" ) ;
385- // Verify enter/exit pairing
386- const redis = w0 . find ( s => s . spanName === "redis_get" ) ;
376+ const { allSpans, spanMeta } = buildSpanData ( customEvents ) ;
377+ if ( allSpans . length !== 2 ) fail ( `Expected 2 spans, got ${ allSpans . length } ` ) ;
378+ const redis = allSpans . find ( s => s . spanName === "redis_get" ) ;
379+ const handle = allSpans . find ( s => s . spanName === "handle_request" ) ;
380+ if ( ! redis || ! handle ) fail ( "Missing expected spans" ) ;
387381 if ( redis . start !== 1100 || redis . end !== 1200 ) fail ( "redis_get timing wrong" ) ;
382+ if ( redis . segments . length !== 1 ) fail ( `Expected 1 segment, got ${ redis . segments . length } ` ) ;
383+ if ( redis . segments [ 0 ] . workerId !== 0 ) fail ( "segment workerId wrong" ) ;
388384 if ( ! spanMeta . has ( 1 ) || ! spanMeta . has ( 2 ) ) fail ( "spanMeta missing entries" ) ;
389- pass ( `${ w0 . length } span intervals paired correctly` ) ;
385+ // Verify sorted by start time
386+ if ( allSpans [ 0 ] . start > allSpans [ 1 ] . start ) fail ( "Spans not sorted by start time" ) ;
387+ pass ( `${ allSpans . length } spans paired correctly` ) ;
390388 }
391389
392390 function testBuildSpanDataParent ( ) {
@@ -396,17 +394,17 @@ async function main() {
396394 { name : "SpanExitEvent" , timestamp : 1200 , fields : { worker_id : 0 , span_id : 20 , span_name : "child" , fields : { } } } ,
397395 { name : "SpanExitEvent" , timestamp : 1300 , fields : { worker_id : 0 , span_id : 10 , span_name : "root" , fields : { } } } ,
398396 ] ;
399- const { spansByWorker } = buildSpanData ( customEvents ) ;
400- const child = spansByWorker [ 0 ] . find ( s => s . spanName === "child" ) ;
397+ const { allSpans } = buildSpanData ( customEvents ) ;
398+ const child = allSpans . find ( s => s . spanName === "child" ) ;
401399 if ( child . parentSpanId !== 10 ) fail ( `Expected parentSpanId=10, got ${ child . parentSpanId } ` ) ;
402- const root = spansByWorker [ 0 ] . find ( s => s . spanName === "root" ) ;
400+ const root = allSpans . find ( s => s . spanName === "root" ) ;
403401 if ( root . parentSpanId !== null ) fail ( `Expected root parentSpanId=null, got ${ root . parentSpanId } ` ) ;
404402 pass ( "Parent span IDs preserved correctly" ) ;
405403 }
406404
407405 function testBuildSpanDataEmpty ( ) {
408- const { spansByWorker , spanMeta } = buildSpanData ( [ ] ) ;
409- if ( Object . keys ( spansByWorker ) . length !== 0 ) fail ( "Expected empty spansByWorker " ) ;
406+ const { allSpans , spanMeta } = buildSpanData ( [ ] ) ;
407+ if ( allSpans . length !== 0 ) fail ( "Expected empty allSpans " ) ;
410408 if ( spanMeta . size !== 0 ) fail ( "Expected empty spanMeta" ) ;
411409 pass ( "Empty input produces empty output" ) ;
412410 }
@@ -421,11 +419,10 @@ async function main() {
421419 { name : "SpanExitEvent" , timestamp : 1400 , fields : { worker_id : 0 , span_id : 2 , span_name : "mid" , fields : { } } } ,
422420 { name : "SpanExitEvent" , timestamp : 1500 , fields : { worker_id : 0 , span_id : 1 , span_name : "root" , fields : { } } } ,
423421 ] ;
424- const { spansByWorker, maxDepth } = buildSpanData ( customEvents ) ;
425- const spans = spansByWorker [ 0 ] ;
426- const root = spans . find ( s => s . spanName === "root" ) ;
427- const mid = spans . find ( s => s . spanName === "mid" ) ;
428- const leaf = spans . find ( s => s . spanName === "leaf" ) ;
422+ const { allSpans, maxDepth } = buildSpanData ( customEvents ) ;
423+ const root = allSpans . find ( s => s . spanName === "root" ) ;
424+ const mid = allSpans . find ( s => s . spanName === "mid" ) ;
425+ const leaf = allSpans . find ( s => s . spanName === "leaf" ) ;
429426 if ( root . depth !== 0 ) fail ( `root depth=${ root . depth } , expected 0` ) ;
430427 if ( mid . depth !== 1 ) fail ( `mid depth=${ mid . depth } , expected 1` ) ;
431428 if ( leaf . depth !== 2 ) fail ( `leaf depth=${ leaf . depth } , expected 2` ) ;
@@ -441,31 +438,31 @@ async function main() {
441438 { name : "SpanExitEvent" , timestamp : 1200 , fields : { worker_id : 0 , span_id : 2 , span_name : "b" , fields : { } } } ,
442439 { name : "SpanExitEvent" , timestamp : 1300 , fields : { worker_id : 0 , span_id : 1 , span_name : "a" , fields : { } } } ,
443440 ] ;
444- const { spansByWorker } = buildSpanData ( customEvents ) ;
445- if ( ! spansByWorker [ 0 ] || spansByWorker [ 0 ] . length !== 2 ) fail ( "Expected 2 spans" ) ;
441+ const { allSpans } = buildSpanData ( customEvents ) ;
442+ if ( allSpans . length !== 2 ) fail ( "Expected 2 spans" ) ;
446443 // Just verify it didn't crash; depths may be arbitrary due to cycle
447444 pass ( "Cyclic parent chain does not stack overflow" ) ;
448445 }
449446
450447 function testBuildSpanDataRecycledId ( ) {
451- // Span ID 1 used first as "alpha", then recycled as "beta"
448+ // Span ID 1 used first as "alpha", closed, then recycled as "beta"
452449 const customEvents = [
453450 { name : "SpanEnterEvent" , timestamp : 1000 , fields : { worker_id : 0 , span_id : 1 , parent_span_id : null , span_name : "alpha" , fields : { } } } ,
454451 { name : "SpanExitEvent" , timestamp : 1100 , fields : { worker_id : 0 , span_id : 1 , span_name : "alpha" , fields : { } } } ,
452+ { name : "SpanCloseEvent" , timestamp : 1150 , fields : { span_id : 1 } } ,
455453 // Same span_id reused with different name
456454 { name : "SpanEnterEvent" , timestamp : 2000 , fields : { worker_id : 0 , span_id : 1 , parent_span_id : null , span_name : "beta" , fields : { } } } ,
457455 { name : "SpanExitEvent" , timestamp : 2100 , fields : { worker_id : 0 , span_id : 1 , span_name : "beta" , fields : { } } } ,
456+ { name : "SpanCloseEvent" , timestamp : 2150 , fields : { span_id : 1 } } ,
458457 // Child of the recycled span
459458 { name : "SpanEnterEvent" , timestamp : 3000 , fields : { worker_id : 0 , span_id : 2 , parent_span_id : 1 , span_name : "child" , fields : { } } } ,
460459 { name : "SpanExitEvent" , timestamp : 3100 , fields : { worker_id : 0 , span_id : 2 , span_name : "child" , fields : { } } } ,
461460 ] ;
462- const { spansByWorker } = buildSpanData ( customEvents ) ;
463- const spans = spansByWorker [ 0 ] ;
464- if ( spans . length !== 3 ) fail ( `Expected 3 spans, got ${ spans . length } ` ) ;
465- const alpha = spans . find ( s => s . spanName === "alpha" ) ;
466- const beta = spans . find ( s => s . spanName === "beta" ) ;
461+ const { allSpans } = buildSpanData ( customEvents ) ;
462+ if ( allSpans . length !== 3 ) fail ( `Expected 3 spans, got ${ allSpans . length } ` ) ;
463+ const alpha = allSpans . find ( s => s . spanName === "alpha" ) ;
464+ const beta = allSpans . find ( s => s . spanName === "beta" ) ;
467465 if ( ! alpha || ! beta ) fail ( "Missing alpha or beta span" ) ;
468- // Both should exist as separate intervals despite same span_id
469466 if ( alpha . start !== 1000 || beta . start !== 2000 ) fail ( "Span intervals not distinct" ) ;
470467 pass ( "Recycled span IDs produce separate intervals" ) ;
471468 }
@@ -477,14 +474,13 @@ async function main() {
477474 { name : "SpanEnter:myapp::handle:src/main.rs:10" , timestamp : 1000 , fields : { worker_id : 0 , span_id : 1 , parent_span_id : null , span_name : "handle" , request_id : "abc-123" } } ,
478475 { name : "SpanExit:myapp::handle:src/main.rs:10" , timestamp : 1100 , fields : { worker_id : 0 , span_id : 1 , span_name : "handle" , request_id : "abc-123" } } ,
479476 ] ;
480- const { spansByWorker } = buildSpanData ( customEvents ) ;
481- const spans = spansByWorker [ 0 ] ;
482- if ( ! spans || spans . length !== 1 ) fail ( `Expected 1 span, got ${ spans ?. length } ` ) ;
483- if ( spans [ 0 ] . spanName !== "handle" ) fail ( `Expected span name 'handle', got '${ spans [ 0 ] . spanName } '` ) ;
484- if ( spans [ 0 ] . fields . request_id !== "abc-123" ) fail ( `Expected request_id='abc-123', got '${ spans [ 0 ] . fields . request_id } '` ) ;
477+ const { allSpans } = buildSpanData ( customEvents ) ;
478+ if ( ! allSpans || allSpans . length !== 1 ) fail ( `Expected 1 span, got ${ allSpans ?. length } ` ) ;
479+ if ( allSpans [ 0 ] . spanName !== "handle" ) fail ( `Expected span name 'handle', got '${ allSpans [ 0 ] . spanName } '` ) ;
480+ if ( allSpans [ 0 ] . fields . request_id !== "abc-123" ) fail ( `Expected request_id='abc-123', got '${ allSpans [ 0 ] . fields . request_id } '` ) ;
485481 // Base fields should NOT appear in the user fields
486- if ( spans [ 0 ] . fields . worker_id ) fail ( "worker_id should not be in user fields" ) ;
487- if ( spans [ 0 ] . fields . span_name ) fail ( "span_name should not be in user fields" ) ;
482+ if ( allSpans [ 0 ] . fields . worker_id ) fail ( "worker_id should not be in user fields" ) ;
483+ if ( allSpans [ 0 ] . fields . span_name ) fail ( "span_name should not be in user fields" ) ;
488484 pass ( "Per-callsite schema with typed fields parsed correctly" ) ;
489485 }
490486
@@ -495,15 +491,39 @@ async function main() {
495491 // This enter has no matching exit (trace ended mid-span)
496492 { name : "SpanEnter:app::b:f:2" , timestamp : 1200 , fields : { worker_id : 0 , span_id : 2 , parent_span_id : null , span_name : "b" } } ,
497493 ] ;
498- const { spansByWorker, unmatchedSpans } = buildSpanData ( customEvents ) ;
499- const matched = spansByWorker [ 0 ] || [ ] ;
500- if ( matched . length !== 1 ) fail ( `Expected 1 matched span, got ${ matched . length } ` ) ;
494+ const { allSpans, unmatchedSpans } = buildSpanData ( customEvents ) ;
495+ if ( allSpans . length !== 1 ) fail ( `Expected 1 matched span, got ${ allSpans . length } ` ) ;
501496 if ( ! unmatchedSpans || unmatchedSpans . length !== 1 ) fail ( `Expected 1 unmatched span, got ${ unmatchedSpans ?. length } ` ) ;
502497 if ( unmatchedSpans [ 0 ] . spanName !== "b" ) fail ( `Expected unmatched span 'b', got '${ unmatchedSpans [ 0 ] . spanName } '` ) ;
503498 if ( unmatchedSpans [ 0 ] . spanId !== 2 ) fail ( `Expected unmatched spanId 2, got ${ unmatchedSpans [ 0 ] . spanId } ` ) ;
504499 pass ( "Unmatched spans (enter without exit) detected correctly" ) ;
505500 }
506501
502+ function testBuildSpanDataMultiplePolls ( ) {
503+ // A span entered/exited multiple times (async future polled 3 times with sleep gap)
504+ const customEvents = [
505+ { name : "SpanEnter:app::f:f:1" , timestamp : 1000 , fields : { worker_id : 0 , span_id : 1 , parent_span_id : null , span_name : "my_fn" } } ,
506+ { name : "SpanExit:app::f:f:1" , timestamp : 1500 , fields : { worker_id : 0 , span_id : 1 , span_name : "my_fn" } } ,
507+ { name : "SpanEnter:app::f:f:1" , timestamp : 100000 , fields : { worker_id : 1 , span_id : 1 , parent_span_id : null , span_name : "my_fn" } } ,
508+ { name : "SpanExit:app::f:f:1" , timestamp : 100200 , fields : { worker_id : 1 , span_id : 1 , span_name : "my_fn" } } ,
509+ { name : "SpanEnter:app::f:f:1" , timestamp : 100300 , fields : { worker_id : 0 , span_id : 1 , parent_span_id : null , span_name : "my_fn" } } ,
510+ { name : "SpanExit:app::f:f:1" , timestamp : 100400 , fields : { worker_id : 0 , span_id : 1 , span_name : "my_fn" } } ,
511+ { name : "SpanCloseEvent" , timestamp : 100500 , fields : { span_id : 1 } } ,
512+ ] ;
513+ const { allSpans } = buildSpanData ( customEvents ) ;
514+ if ( allSpans . length !== 1 ) fail ( `Expected 1 span, got ${ allSpans . length } ` ) ;
515+ const s = allSpans [ 0 ] ;
516+ if ( s . segments . length !== 3 ) fail ( `Expected 3 segments, got ${ s . segments . length } ` ) ;
517+ if ( s . start !== 1000 ) fail ( `Expected start=1000, got ${ s . start } ` ) ;
518+ if ( s . end !== 100400 ) fail ( `Expected end=100400, got ${ s . end } ` ) ;
519+ // activeNs = 500 + 200 + 100 = 800
520+ if ( s . activeNs !== 800 ) fail ( `Expected activeNs=800, got ${ s . activeNs } ` ) ;
521+ // Workers: polled on both 0 and 1
522+ const workers = [ ...new Set ( s . segments . map ( seg => seg . workerId ) ) ] . sort ( ) ;
523+ if ( workers . length !== 2 || workers [ 0 ] !== 0 || workers [ 1 ] !== 1 ) fail ( `Expected workers [0,1], got ${ workers } ` ) ;
524+ pass ( "Multiple polls grouped into single span with segments" ) ;
525+ }
526+
507527 // ── Regression: open PollStart at trace end must not create phantom poll (#194) ──
508528
509529 function testOpenPollStartDiscarded ( ) {
@@ -576,6 +596,7 @@ async function main() {
576596 testBuildSpanDataRecycledId ( ) ;
577597 testBuildSpanDataPerCallsiteSchema ( ) ;
578598 testBuildSpanDataUnmatched ( ) ;
599+ testBuildSpanDataMultiplePolls ( ) ;
579600
580601 console . log ( "\n✓ All analysis checks passed!" ) ;
581602}
0 commit comments