Skip to content
This repository was archived by the owner on Dec 17, 2024. It is now read-only.

Commit 51476a1

Browse files
committed
add simple timeline variant of grid
Fixes #883
1 parent 59da69e commit 51476a1

File tree

8 files changed

+311
-28
lines changed

8 files changed

+311
-28
lines changed

app/plugins/modules/activation-visualizations/lib/cell.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,16 @@ exports.renderCell = (returnTo, cell, activation, isFailure=!activation.response
6060
fdom.className = 'grid-oops-overlay'
6161
container.appendChild(fdom)
6262

63-
} else if (!options || options.zoom >= 0) {
63+
} else /*if (!options || options.zoom >= 0)*/ {
6464
// for larger zoom levels, and only for successful activations,
6565
// render the latency inside the cell
6666
const innerLabel = document.createElement('span')
6767
innerLabel.innerText = prettyPrintDuration(duration)
6868
container.appendChild(innerLabel)
6969

70-
} else {
70+
}
71+
72+
if (options && options.zoom < 0) {
7173
// for higher zoom levels (zoom < 0), render the latency in the tooltip
7274
extraTooltip += `${newline}${prettyPrintDuration(duration)}`
7375
}
@@ -93,8 +95,8 @@ exports.renderCell = (returnTo, cell, activation, isFailure=!activation.response
9395
cell.isFailure = isFailure
9496
cell.setAttribute('data-action-name', activation.name)
9597
cell.setAttribute('data-balloon-break', 'data-balloon-break')
96-
cell.setAttribute('data-balloon', `${options && options.nameInTooltip ? activation.name + ' action, invoked ' : ''}${ui.prettyPrintTime(activation.start, 'short')}${msg}${extraTooltip}`)
97-
cell.setAttribute('data-balloon-pos', 'up')
98+
cell.setAttribute('data-balloon', `${options && options.nameInTooltip ? 'Action: ' + activation.name + newline : ''}${ui.prettyPrintTime(activation.start, 'short')}${msg}${extraTooltip}`)
99+
cell.setAttribute('data-balloon-pos', options.balloonPos || 'up')
98100
}
99101

100102
return returnValue

app/plugins/modules/activation-visualizations/lib/grid.js

Lines changed: 142 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
const prettyPrintDuration = require('pretty-ms'),
1818
{ sort, sortActivations, startTimeSorter, nameSorter, countSorter } = require('./sorting'),
1919
{ drilldownWith } = require('./drilldown'),
20-
{ groupByAction } = require('./grouping'),
20+
{ groupByAction, groupByTimeBucket } = require('./grouping'),
2121
{ drawLegend } = require('./legend'),
2222
{ renderCell } = require('./cell'),
2323
{ modes } = require('./modes'),
2424
{ grid:usage } = require('../usage'),
25-
{ nbsp, optionsToString, isSuccess, titleWhenNothingSelected, latencyBucket,
25+
{ nbsp, optionsToString, isSuccess, titleWhenNothingSelected, latencyBucket, nLatencyBuckets,
2626
displayTimeRange, prepareHeader, visualize } = require('./util')
2727

2828
const viewName = 'Grid'
@@ -152,7 +152,9 @@ const drawGrid = (options, header) => activations => {
152152
redraw = !!existingContent
153153

154154
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)
156158

157159
//injectHTML(content, 'grid/bottom-bar.html', 'bottom-bar')
158160

@@ -178,8 +180,10 @@ const drawGrid = (options, header) => activations => {
178180
width = gridDom.getAttribute('data-width'),
179181
vws = newZoom === 0 ? 2.75 : newZoom === 1 ? 3 : newZoom === 2 ? 4 : 0.75
180182

181-
gridLabel.style.maxWidth = `${Math.max(8, width * vws * 1.1)}vw`
182183
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+
}
183187
}
184188

185189
if (newZoom === zoomMax) {
@@ -207,15 +211,31 @@ const drawGrid = (options, header) => activations => {
207211
flush: 'right',
208212
actAsButton: true,
209213
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
211231

212232
return {
213233
type: 'custom',
214234
content,
215235
controlHeaders: true,
216236

217237
// 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])
219239
}
220240
}
221241

@@ -241,7 +261,7 @@ const smartZoom = numCells => {
241261
*
242262
*/
243263
const _drawGrid = (options, {sidecar, leftHeader, rightHeader}, content, groupData, sorter=countSorter, sortDir=+1, redraw) => {
244-
const { groups, summary } = groupData
264+
const { groups, summary, timeline } = groupData
245265

246266
sort(groups, sorter, sortDir)
247267
sortActivations(groups, startTimeSorter, +1)
@@ -251,7 +271,7 @@ const _drawGrid = (options, {sidecar, leftHeader, rightHeader}, content, groupDa
251271
gridGrid = redraw ? content.querySelector(`.${css.gridGrid}`) : document.createElement('div'),
252272
totalCount = groupData.totalCount,
253273
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
255275

256276
gridGrid.className = `${css.gridGrid} cell-container zoom_${zoomLevelForDisplay}`
257277
gridGrid.setAttribute('data-zoom-level', zoomLevelForDisplay)
@@ -287,6 +307,11 @@ const _drawGrid = (options, {sidecar, leftHeader, rightHeader}, content, groupDa
287307
// add time range to the sidecar header
288308
displayTimeRange(groupData, leftHeader)
289309

310+
if (options.timeline) {
311+
drawAsTimeline(timeline, content, gridGrid, zoomLevelForDisplay, options)
312+
return
313+
}
314+
290315
groups.forEach((group, groupIdx) => {
291316
// prepare the grid structure
292317
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
372397
const cell = makeCellDom()
373398
cellContainer.appendChild(cell)
374399
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,
376403
{ zoom: zoomLevelForDisplay })
377404
}
378405
} catch (e) {
@@ -381,8 +408,114 @@ const _drawGrid = (options, {sidecar, leftHeader, rightHeader}, content, groupDa
381408
})
382409
}
383410
})
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)
384425
}
385426

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+
386519
/**
387520
* This is the module
388521
*

app/plugins/modules/activation-visualizations/lib/grouping.js

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,29 @@
1717
const { isSuccess, pathOf, latencyBucket, nLatencyBuckets, isUUIDPattern } = require('./util'),
1818
prettyPrintDuration = require('pretty-ms')
1919

20+
const durationOf = _ => {
21+
const waitAnno = _.annotations.find(({key}) => key === 'waitTime'),
22+
initAnno = _.annotations.find(({key}) => key === 'initTime'),
23+
wait = waitAnno ? waitAnno.value : 0, // this is "Queueing Time" as presented in the UI
24+
init = initAnno ? initAnno.value : 0, // and this is "Container Initialization"
25+
executionTime = _.end - _.start,
26+
duration = executionTime + wait // note: executionTime already factors in `init`, so we don't add it here
27+
28+
// oof
29+
_.executionTime = executionTime
30+
_._duration = duration
31+
32+
return { duration, executionTime, wait, init }
33+
}
34+
2035
/**
2136
* Compute statistical properties of a given group of activations
2237
*
2338
*/
2439
const summarizePerformance = (activations, options) => {
2540
const latBuckets = Array(nLatencyBuckets).fill(0)
2641
const summaries = activations.map(_ => {
27-
const waitAnno = _.annotations.find(({key}) => key === 'waitTime'),
28-
initAnno = _.annotations.find(({key}) => key === 'initTime'),
29-
wait = waitAnno ? waitAnno.value : 0, // this is "Queueing Time" as presented in the UI
30-
init = initAnno ? initAnno.value : 0, // and this is "Container Initialization"
31-
executionTime = _.end - _.start,
32-
duration = executionTime + wait // note: executionTime already factors in `init`, so we don't add it here
42+
const { duration, executionTime, wait, init } = durationOf(_)
3343

3444
if (isSuccess(_)) {
3545
const latBucket = latencyBucket(options.full ? duration : executionTime)
@@ -296,7 +306,7 @@ const costOf = activation => {
296306
* Construct a success versus failure timeline model
297307
*
298308
*/
299-
const successFailureTimeline = (activations, { nBuckets = 20 }) => {
309+
const successFailureTimeline = (activations, { nBuckets=10000, full=false }) => {
300310
if (activations.length === 0) {
301311
return []
302312
}
@@ -307,14 +317,28 @@ const successFailureTimeline = (activations, { nBuckets = 20 }) => {
307317
interval = ~~((last - first) / nBuckets),
308318
bucketize = timestamp => Math.min(nBuckets - 1, ~~((timestamp - first) / interval))
309319

320+
const mkActivations = () => {
321+
const buckets = Array(nBuckets)
322+
for (let idx = 0; idx < nBuckets; idx++) {
323+
buckets[idx] = [] // array of activations
324+
}
325+
return buckets
326+
}
327+
310328
// now we construct the model
311329
const buckets = activations.reduce((buckets, activation) => {
312-
const tally = isSuccess(activation) ? buckets.success : buckets.failure,
330+
const success = isSuccess(activation),
331+
tally = success ? buckets.success : buckets.failure,
313332
idx = bucketize(activation.start)
333+
314334
tally[idx]++
315335
buckets.cost[idx] += costOf(activation)
336+
buckets.activations[idx].push(activation)
337+
316338
return buckets
339+
317340
}, { success: Array(nBuckets).fill(0),
341+
activations: mkActivations(),
318342
failure: Array(nBuckets).fill(0),
319343
cost: Array(nBuckets).fill(0),
320344
interval, first, last, nBuckets // pass through the parameters to the view, in case it helps
@@ -375,3 +399,56 @@ const filterByOutlieriness = options => ({activations,statData}) => {
375399
})
376400
}
377401
}
402+
403+
404+
/**
405+
* Group the given activations by time
406+
*
407+
*/
408+
exports.groupByTimeBucket = (activations, options) => {
409+
// commenting out the bizarre filter. see shell issue #120
410+
/*if (!options.all) {
411+
activations = activations.filter(activation => {
412+
const path = pathOf(activation)
413+
return !(path.match && path.match(isUUIDPattern)) && !activation.cause
414+
})
415+
}*/
416+
417+
// first, sort the activations by increasing start time, to help
418+
// with bucketing
419+
activations.sort((a,b) => a.start - b.start)
420+
421+
// compute bucket properties
422+
const nBuckets = options.buckets || 46,
423+
first = activations[0],
424+
last = activations[activations.length - 1],
425+
minTime = first && first.start,
426+
maxTime = last && last.start,
427+
timeRangeInMillis = maxTime - minTime + 1,
428+
bucketWidthInMillis = timeRangeInMillis / nBuckets,
429+
totals = { minTime: undefined, maxTime: undefined, totalCount: 0},
430+
grouper = addToGroup(options, totals)
431+
432+
const buckets = activations.reduce((bucketArray, activation) => {
433+
const bucketIdx = ~~( (activation.start - minTime) / bucketWidthInMillis)
434+
grouper(bucketArray[bucketIdx], activation)
435+
return bucketArray
436+
}, new Array(nBuckets).fill(0).map(_ => ({}) )) // an array of length nBuckets, of {} -- these will be activation groups, for each timeline bucket
437+
438+
439+
// the buckets.map turns each timeline bucket, which right now is
440+
// a map from action path to action, into an array -- for easier
441+
// consumption
442+
return Object.assign(totals, {
443+
bucketWidthInMillis,
444+
buckets: buckets.map(bucketMap => {
445+
const bucket = toArray(bucketMap, options)
446+
return {
447+
bucket,
448+
summary: summarizeWhole(bucket, options)
449+
}
450+
}),
451+
summary: summarizeWhole2(activations, options) // a "statData" object, for all activations
452+
})
453+
}
454+

app/plugins/modules/activation-visualizations/lib/time.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ exports.range = options => {
4242
startOfWeek = (startOfToday - date.getDay() * oneDay)
4343

4444
let since, upto
45-
if (options.today || options.t) {
45+
if (options.today) {
4646
since = startOfToday
4747
upto = now
4848

0 commit comments

Comments
 (0)