@@ -17,15 +17,67 @@ interface TimelineProps {
1717 onTimeScaleChange : ( scale : number ) => void ;
1818}
1919
20- function getWorkerNames ( session : BuildSession ) : string [ ] {
21- const seen = new Map < string , string > ( ) ;
22- for ( const job of session . jobs ) {
20+ interface CoreRow {
21+ hostName : string ;
22+ coreIndex : number ;
23+ jobs : BuildJob [ ] ;
24+ }
25+
26+ // Assign jobs to virtual "cores" per host via first-fit scheduling, so that
27+ // each row in the timeline represents a single execution slot that runs at
28+ // most one job at a time. The number of cores per host therefore equals the
29+ // peak number of concurrent jobs that ran on that host.
30+ function getCoreRows ( session : BuildSession ) : CoreRow [ ] {
31+ const sortedJobs = [ ...session . jobs ] . sort ( ( a , b ) => a . startTime - b . startTime ) ;
32+
33+ interface CoreSlot { jobs : BuildJob [ ] ; lastEndTime : number ; }
34+ const hostCores = new Map < string , CoreSlot [ ] > ( ) ;
35+ const displayNameByKey = new Map < string , string > ( ) ;
36+
37+ for ( const job of sortedJobs ) {
2338 const key = job . hostName . toLowerCase ( ) ;
24- if ( ! seen . has ( key ) ) {
25- seen . set ( key , job . hostName ) ;
39+ if ( ! displayNameByKey . has ( key ) ) {
40+ displayNameByKey . set ( key , job . hostName ) ;
41+ }
42+ let cores = hostCores . get ( key ) ;
43+ if ( ! cores ) {
44+ cores = [ ] ;
45+ hostCores . set ( key , cores ) ;
2646 }
47+
48+ const jobEnd = job . endTime ?? Date . now ( ) ;
49+ let assigned = false ;
50+ for ( const core of cores ) {
51+ if ( core . lastEndTime <= job . startTime ) {
52+ core . jobs . push ( job ) ;
53+ core . lastEndTime = jobEnd ;
54+ assigned = true ;
55+ break ;
56+ }
57+ }
58+ if ( ! assigned ) {
59+ cores . push ( { jobs : [ job ] , lastEndTime : jobEnd } ) ;
60+ }
61+ }
62+
63+ // Local host first, then remaining hosts alphabetically.
64+ const hostKeys = Array . from ( hostCores . keys ( ) ) . sort ( ( a , b ) => {
65+ const aLocal = a === 'local' ;
66+ const bLocal = b === 'local' ;
67+ if ( aLocal && ! bLocal ) return - 1 ;
68+ if ( ! aLocal && bLocal ) return 1 ;
69+ return a . localeCompare ( b ) ;
70+ } ) ;
71+
72+ const rows : CoreRow [ ] = [ ] ;
73+ for ( const key of hostKeys ) {
74+ const cores = hostCores . get ( key ) ! ;
75+ const displayName = displayNameByKey . get ( key ) || key ;
76+ cores . forEach ( ( core , idx ) => {
77+ rows . push ( { hostName : displayName , coreIndex : idx , jobs : core . jobs } ) ;
78+ } ) ;
2779 }
28- return Array . from ( seen . values ( ) ) ;
80+ return rows ;
2981}
3082
3183function getMaxTime ( session : BuildSession ) : number {
@@ -137,8 +189,8 @@ export default function Timeline({
137189
138190 const dpr = window . devicePixelRatio || 1 ;
139191 const containerWidth = container . clientWidth ;
140- const workerNames = session ? getWorkerNames ( session ) : [ ] ;
141- const canvasHeight = Math . max ( 200 , HEADER_HEIGHT + workerNames . length * ROW_HEIGHT + PAD * 2 ) ;
192+ const coreRows = session ? getCoreRows ( session ) : [ ] ;
193+ const canvasHeight = Math . max ( 200 , HEADER_HEIGHT + coreRows . length * ROW_HEIGHT + PAD * 2 ) ;
142194
143195 canvas . style . width = containerWidth + 'px' ;
144196 canvas . style . height = canvasHeight + 'px' ;
@@ -150,7 +202,7 @@ export default function Timeline({
150202 ctx . fillStyle = '#1E1E1E' ;
151203 ctx . fillRect ( 0 , 0 , containerWidth , canvasHeight ) ;
152204
153- if ( ! session || workerNames . length === 0 ) {
205+ if ( ! session || coreRows . length === 0 ) {
154206 ctx . fillStyle = '#888' ;
155207 ctx . font = '16px sans-serif' ;
156208 ctx . textAlign = 'center' ;
@@ -210,9 +262,9 @@ export default function Timeline({
210262 ctx . stroke ( ) ;
211263 }
212264
213- // Worker rows
214- for ( let i = 0 ; i < workerNames . length ; i ++ ) {
215- const wName = workerNames [ i ] ;
265+ // Core rows (one row per virtual CPU core per host)
266+ for ( let i = 0 ; i < coreRows . length ; i ++ ) {
267+ const row = coreRows [ i ] ;
216268 const y = HEADER_HEIGHT + i * ROW_HEIGHT ;
217269
218270 // Alternate background
@@ -229,11 +281,8 @@ export default function Timeline({
229281 ctx . lineTo ( containerWidth , y + ROW_HEIGHT ) ;
230282 ctx . stroke ( ) ;
231283
232- // Draw jobs for this worker
233- const jobs = session . jobs . filter (
234- j => j . hostName . toLowerCase ( ) === wName . toLowerCase ( )
235- ) ;
236- for ( const job of jobs ) {
284+ // Draw jobs scheduled on this core
285+ for ( const job of row . jobs ) {
237286 drawJob ( ctx , job , startTime , y , containerWidth , timeScale , horizontalOffset ) ;
238287 }
239288
@@ -245,12 +294,13 @@ export default function Timeline({
245294 }
246295 ctx . fillRect ( 0 , y , LABEL_WIDTH , ROW_HEIGHT ) ;
247296
248- // Worker label
297+ // Core label
249298 ctx . fillStyle = '#CCC' ;
250299 ctx . font = '11px sans-serif' ;
251300 ctx . textAlign = 'left' ;
252301 ctx . textBaseline = 'middle' ;
253- ctx . fillText ( wName , 4 , y + ROW_HEIGHT / 2 , LABEL_WIDTH - 8 ) ;
302+ const label = `${ row . hostName } (Core # ${ row . coreIndex } )` ;
303+ ctx . fillText ( label , 4 , y + ROW_HEIGHT / 2 , LABEL_WIDTH - 8 ) ;
254304 }
255305
256306 // Label column separator
@@ -301,21 +351,17 @@ export default function Timeline({
301351 const dpr = window . devicePixelRatio || 1 ;
302352 const mx = ( e . clientX - rect . left ) * dpr ;
303353 const my = ( e . clientY - rect . top ) * dpr ;
304- const workerNames = getWorkerNames ( session ) ;
354+ const coreRows = getCoreRows ( session ) ;
305355 const startTime = session . startTime ;
306356
307357 let tooltipJob : BuildJob | undefined = undefined ;
308358
309- for ( let i = 0 ; i < workerNames . length ; i ++ ) {
359+ for ( let i = 0 ; i < coreRows . length ; i ++ ) {
310360 const yTop = ( HEADER_HEIGHT + i * ROW_HEIGHT + PAD ) * dpr ;
311361 const yBot = ( HEADER_HEIGHT + i * ROW_HEIGHT + ROW_HEIGHT - PAD ) * dpr ;
312362 if ( my < yTop || my > yBot ) continue ;
313363
314- const wName = workerNames [ i ] ;
315- const jobs = session . jobs . filter (
316- j => j . hostName . toLowerCase ( ) === wName . toLowerCase ( )
317- ) ;
318- for ( const job of jobs ) {
364+ for ( const job of coreRows [ i ] . jobs ) {
319365 const jobStart = ( job . startTime - startTime ) / 1000 ;
320366 const jobEnd = ( ( job . endTime || Date . now ( ) ) - startTime ) / 1000 ;
321367 const x = ( LABEL_WIDTH + jobStart * timeScale - horizontalOffset ) * dpr ;
0 commit comments