17
17
const prettyPrintDuration = require ( 'pretty-ms' ) ,
18
18
{ sort, sortActivations, startTimeSorter, nameSorter, countSorter } = require ( './sorting' ) ,
19
19
{ drilldownWith } = require ( './drilldown' ) ,
20
- { groupByAction } = require ( './grouping' ) ,
20
+ { groupByAction, groupByTimeBucket } = require ( './grouping' ) ,
21
21
{ drawLegend } = require ( './legend' ) ,
22
22
{ renderCell } = require ( './cell' ) ,
23
23
{ modes } = require ( './modes' ) ,
24
24
{ grid :usage } = require ( '../usage' ) ,
25
- { nbsp, optionsToString, isSuccess, titleWhenNothingSelected, latencyBucket,
25
+ { nbsp, optionsToString, isSuccess, titleWhenNothingSelected, latencyBucket, nLatencyBuckets ,
26
26
displayTimeRange, prepareHeader, visualize } = require ( './util' )
27
27
28
28
const viewName = 'Grid'
@@ -152,7 +152,9 @@ const drawGrid = (options, header) => activations => {
152
152
redraw = ! ! existingContent
153
153
154
154
content . className = css . content
155
- _drawGrid ( options , header , content , groupByAction ( activations , options ) , undefined , undefined , redraw )
155
+ _drawGrid ( options , header , content ,
156
+ groupByAction ( activations , options ) ,
157
+ undefined , undefined , redraw )
156
158
157
159
//injectHTML(content, 'grid/bottom-bar.html', 'bottom-bar')
158
160
@@ -178,8 +180,10 @@ const drawGrid = (options, header) => activations => {
178
180
width = gridDom . getAttribute ( 'data-width' ) ,
179
181
vws = newZoom === 0 ? 2.75 : newZoom === 1 ? 3 : newZoom === 2 ? 4 : 0.75
180
182
181
- gridLabel . style . maxWidth = `${ Math . max ( 8 , width * vws * 1.1 ) } vw`
182
183
gridRow . style . maxWidth = `${ Math . max ( 8 , width * vws ) } vw`
184
+ if ( gridLabel ) {
185
+ gridLabel . style . maxWidth = `${ Math . max ( 8 , width * vws * 1.1 ) } vw`
186
+ }
183
187
}
184
188
185
189
if ( newZoom === zoomMax ) {
@@ -207,15 +211,31 @@ const drawGrid = (options, header) => activations => {
207
211
flush : 'right' ,
208
212
actAsButton : true ,
209
213
direct : rezoom ( _ => Math . max ( - 2 , _ - 1 ) )
210
- }
214
+ } ,
215
+ asTimeline = { mode : 'as-timeline' ,
216
+ fontawesome : 'fas fa-chart-bar' ,
217
+ balloon : 'Display as timeline' ,
218
+ flush : 'right' ,
219
+ actAsButton : true ,
220
+ direct : ( ) => repl . pexec ( `grid ${ optionsToString ( options ) } -t` )
221
+ } ,
222
+ asGrid = { mode : 'as-grid' ,
223
+ fontawesome : 'fas fa-th' ,
224
+ balloon : 'Display as grid' ,
225
+ flush : 'right' ,
226
+ actAsButton : true ,
227
+ direct : ( ) => repl . pexec ( `grid ${ optionsToString ( options , [ 'timeline' , 't' ] ) } ` )
228
+ } ,
229
+
230
+ switcher = options . timeline ? asGrid : asTimeline // switch between timeline and grid mode
211
231
212
232
return {
213
233
type : 'custom' ,
214
234
content,
215
235
controlHeaders : true ,
216
236
217
237
// add zoom buttons to the mode button model
218
- modes : modes ( 'grid' , options ) . concat ( [ zoomIn , zoomOut ] )
238
+ modes : modes ( 'grid' , options ) . concat ( [ switcher , zoomIn , zoomOut ] )
219
239
}
220
240
}
221
241
@@ -241,7 +261,7 @@ const smartZoom = numCells => {
241
261
*
242
262
*/
243
263
const _drawGrid = ( options , { sidecar, leftHeader, rightHeader} , content , groupData , sorter = countSorter , sortDir = + 1 , redraw ) => {
244
- const { groups, summary } = groupData
264
+ const { groups, summary, timeline } = groupData
245
265
246
266
sort ( groups , sorter , sortDir )
247
267
sortActivations ( groups , startTimeSorter , + 1 )
@@ -251,7 +271,7 @@ const _drawGrid = (options, {sidecar, leftHeader, rightHeader}, content, groupDa
251
271
gridGrid = redraw ? content . querySelector ( `.${ css . gridGrid } ` ) : document . createElement ( 'div' ) ,
252
272
totalCount = groupData . totalCount ,
253
273
zoomLevel = options . zoom || smartZoom ( totalCount ) ,
254
- zoomLevelForDisplay = totalCount > 1000 ? - 2 : totalCount <= 100 ? zoomLevel : 0 // don't zoom in too far, if there are many cells to display
274
+ zoomLevelForDisplay = options . timeline ? - 1 : totalCount > 1000 ? - 2 : totalCount <= 100 ? zoomLevel : 0 // don't zoom in too far, if there are many cells to display
255
275
256
276
gridGrid . className = `${ css . gridGrid } cell-container zoom_${ zoomLevelForDisplay } `
257
277
gridGrid . setAttribute ( 'data-zoom-level' , zoomLevelForDisplay )
@@ -287,6 +307,11 @@ const _drawGrid = (options, {sidecar, leftHeader, rightHeader}, content, groupDa
287
307
// add time range to the sidecar header
288
308
displayTimeRange ( groupData , leftHeader )
289
309
310
+ if ( options . timeline ) {
311
+ drawAsTimeline ( timeline , content , gridGrid , zoomLevelForDisplay , options )
312
+ return
313
+ }
314
+
290
315
groups . forEach ( ( group , groupIdx ) => {
291
316
// prepare the grid structure
292
317
const gridDom = redraw ? gridGrid . querySelector ( `.grid[data-action-path="${ group . path } "]` ) : document . createElement ( 'div' )
@@ -372,7 +397,9 @@ const _drawGrid = (options, {sidecar, leftHeader, rightHeader}, content, groupDa
372
397
const cell = makeCellDom ( )
373
398
cellContainer . appendChild ( cell )
374
399
cell . classList . add ( 'grid-cell-newly-created' )
375
- renderCell ( viewName , cell , activation , ! isSuccess ( activation ) , undefined , undefined ,
400
+ renderCell ( viewName , cell , activation , ! isSuccess ( activation ) ,
401
+ options . full ? activation . _duration : activation . executionTime ,
402
+ undefined ,
376
403
{ zoom : zoomLevelForDisplay } )
377
404
}
378
405
} catch ( e ) {
@@ -381,8 +408,114 @@ const _drawGrid = (options, {sidecar, leftHeader, rightHeader}, content, groupDa
381
408
} )
382
409
}
383
410
} )
411
+ } // _drawGrid
412
+
413
+ /**
414
+ * Return the minimum timestamp in the given list of activations
415
+ *
416
+ */
417
+ const minTimestamp = activations => {
418
+ return activations . reduce ( ( min , activation ) => {
419
+ if ( min === 0 ) {
420
+ return activation . start
421
+ } else {
422
+ return Math . min ( min , activation . start )
423
+ }
424
+ } , 0 )
384
425
}
385
426
427
+ /**
428
+ * Render the grid as a timeline
429
+ *
430
+ */
431
+ const drawAsTimeline = ( timelineData , content , gridGrid , zoomLevelForDisplay , options ) => {
432
+ const { failure, activations, nBuckets } = timelineData
433
+
434
+ content . classList . add ( 'grid-as-timeline' )
435
+
436
+ const grid = document . createElement ( 'div' )
437
+ grid . className = 'grid'
438
+ gridGrid . appendChild ( grid )
439
+
440
+ const makeColumn = ( ) => {
441
+ const gridRow = document . createElement ( 'div' )
442
+ gridRow . className = 'grid-row'
443
+ grid . appendChild ( gridRow )
444
+
445
+ return gridRow
446
+ }
447
+
448
+ // for each column in the timeline... idx here is a column index
449
+ for ( let idx = 0 , currentEmptyRunLength = 0 , currentRunMinTime ; idx < nBuckets ; idx ++ ) {
450
+ if ( activations [ idx ] . length === 0 ) {
451
+ // empty column
452
+ if ( currentEmptyRunLength ++ === 0 && idx > 0 ) {
453
+ // start of empty run; remember the timestamp
454
+ currentRunMinTime = minTimestamp ( activations [ idx - 1 ] )
455
+ }
456
+
457
+ continue
458
+
459
+ } else if ( currentEmptyRunLength > 5 ) {
460
+ console . error ( 'EMPTY SWATH' )
461
+ const currentRunMaxTime = minTimestamp ( activations [ idx ] ) ,
462
+ swath = makeColumn ( )
463
+
464
+ swath . classList . add ( 'grid-timeline-empty-swath' )
465
+
466
+ if ( currentRunMinTime && currentRunMaxTime ) {
467
+ const swathInner = document . createElement ( 'div' )
468
+ swathInner . classList . add ( 'grid-timeline-empty-swath-inner' )
469
+ swathInner . innerText = `${ prettyPrintDuration ( currentRunMaxTime - currentRunMinTime , { compact : true } ) } gap`
470
+
471
+ swath . appendChild ( swathInner )
472
+ }
473
+
474
+ currentEmptyRunLength = 0
475
+ }
476
+
477
+ const gridRow = makeColumn ( )
478
+
479
+ // sort the activations in the column, according to the user's desire
480
+ if ( options . timeline === true || options . timeline === 'latency' ) {
481
+ // default sort order
482
+ activations [ idx ] . sort ( ( a , b ) => {
483
+ const successA = isSuccess ( a ) ,
484
+ successB = isSuccess ( b ) ,
485
+ nA = options . full ? a . _duration : a . executionTime ,
486
+ nB = options . full ? b . _duration : b . executionTime
487
+ return ! successA && ! successB || successA && successB ? nA - nB
488
+ : ! successA ? 1 : - 1
489
+ } )
490
+ } else if ( options . timeline === 'time' ) {
491
+ activations [ idx ] . sort ( ( a , b ) => a . start - b . start )
492
+ }
493
+
494
+ // now render the cells in the column; jdx here is a row index
495
+ // within the current column's stack of cells
496
+ activations [ idx ] . forEach ( ( activation , jdx ) => {
497
+ const success = isSuccess ( activation ) ,
498
+ latBucket = success && latencyBucket ( options . full ? activation . _duration : activation . executionTime )
499
+
500
+ const cell = makeCellDom ( ) ,
501
+ isFailure = false ,
502
+ duration = 0 ,
503
+ nameInTooltip = true ,
504
+ balloonPos = jdx >= 25 ? idx < 5 ? 'down-left' : 'down'
505
+ : idx < 10 ? jdx < 5 ? 'up-left' : 'up' : jdx < 5 ? 'up-right' : 'up'
506
+
507
+ renderCell ( viewName , cell ,
508
+ activation ,
509
+ ! success ,
510
+ options . full ? activation . _duration : activation . executionTime ,
511
+ latBucket ,
512
+ { zoom : zoomLevelForDisplay , balloonPos, nameInTooltip } )
513
+
514
+ gridRow . appendChild ( cell )
515
+ } )
516
+ }
517
+ } // drawAsTimeline
518
+
386
519
/**
387
520
* This is the module
388
521
*
0 commit comments