diff --git a/index.html b/index.html index 7c83a91..57ed8ec 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,7 @@ +
@@ -39,6 +40,12 @@ + + diff --git a/procmonAnalyze.css b/procmonAnalyze.css index ac50585..29d9fa5 100644 --- a/procmonAnalyze.css +++ b/procmonAnalyze.css @@ -26,7 +26,7 @@ body { .color-by { position: absolute; - left: calc(50% - 300px); + left: calc(45% - 300px); top: 64px; } @@ -34,6 +34,10 @@ body { float: left; } +#canvas-markers { + float: left; +} + #fsmap-canvas { float: left; } diff --git a/procmonAnalyze.js b/procmonAnalyze.js index edf2cc6..e8ae9c7 100644 --- a/procmonAnalyze.js +++ b/procmonAnalyze.js @@ -15,6 +15,9 @@ const HOVERED_ENTRY_FILL = 0.9; const FILTERED_OUT_ENTRY_FILL = 0.7; const MAX_DETAIL_LINES = 24; const ASSUMED_CLUSTER_SIZE = 4096; +const TIMELINE_SELECTION_OVERLAY_DEPTH = 0.8; +const TIMELINE_UNSELECTED_GREY_TOP = `#d6d5d2`; +const TIMELINE_UNSELECTED_GREY_BOTTOM = `#d6d5d1`; const csvInput = document.getElementById("csvfile"); const diskifyInput = document.getElementById("diskifyfile"); @@ -22,9 +25,24 @@ const profilerInput = document.getElementById("profilerfile"); const tooltip = document.getElementById("tooltip"); const searchbar = document.getElementById("searchbar-input"); const colorBySelect = document.getElementById("color-by-select"); +const refocus = document.getElementById("redraw") +const reset = document.getElementById("reset"); const timeline = document.getElementById("timeline"); const canvas = document.getElementById("canvas"); const fsmapCanvas = document.getElementById("fsmap-canvas"); +const markersCanvas = document.getElementById("canvas-markers"); +var data, diskifyData; // Contains the entire array of data from input file + +var offsetX, offsetY; +reOffset(); +window.onscroll=function(e){ reOffset(); } +window.onresize=function(e){ reOffset(); } +var isDownTopSlider = false; +var isDownBottomSlider = false; +var topRange, bottomRange; + +var markerCtx; + const diskmapCanvas = document.getElementById("diskmap-canvas"); const readInfo = document.getElementById("read-info"); const tools = document.getElementById("tools"); @@ -42,6 +60,9 @@ fsmapCanvas.height = window.innerHeight - 16; diskmapCanvas.width = window.innerWidth * 0.025; diskmapCanvas.height = window.innerHeight - 16; +markersCanvas.width = window.innerWidth * 0.01; +markersCanvas.height = window.innerHeight - 16; + const VIEWPORT_BUFFER = canvas.height; let headerMap = { @@ -87,6 +108,7 @@ const COLOR_BY_TID = 3; let colorBy = getColorByKey(); let gState = null; +let gOuterState = null; function colorArrayToHex(colorArray) { function denormalizeAndStringify(c) { @@ -184,7 +206,7 @@ function parseReadDetail(detail) { } } -async function drawData(data, diskify) { +async function drawData(aData, diskify, isRefocused) { document.getElementById("chooserWrapper").style.display = "none"; tools.style.display = "block"; @@ -195,7 +217,7 @@ async function drawData(data, diskify) { let totalTimeByOperation = {}; let readsByPath = {}; - for (let row of data) { + for (let row of aData) { let { operation, path, @@ -214,7 +236,7 @@ async function drawData(data, diskify) { if (start < minTime) { if (minTime != Number.MAX_VALUE) { throw new Error("Data should be ordered by start time."); - } + } minTime = start; } if (end > maxTime) { @@ -334,45 +356,69 @@ async function drawData(data, diskify) { let totalTime = maxTime - minTime; let trackWidth = canvas.width / tracks.length; let rendererScale = canvas.height / totalTime; + + if (!isRefocused) { + gOuterState = { + minTime, + maxTime, + totalTime, + unconstrainedData: aData, + unconstrainedDiskifyData: aData, + }; + } + gState = { - minTime, - maxTime, - tracks, - totalTime, - trackWidth, - rendererScale, - targetRendererScale: rendererScale, - diskmapScale, - targetDiskmapScale: diskmapScale, - readsByPath, - diskify, - maxLcn, - diskmapTranslate: 0, - targetDiskmapTranslate: 0, - rendererTranslate: 0, - targetRendererTranslate: 0, - mouseX: 0, - mouseY: 0, - timelineIndicators: [], - lcnReads: [], - lastHoveredRect: null, - selectedEntry: null, - activePath: null, + minTime, + maxTime, + tracks, + totalTime, + trackWidth, + rendererScale, + targetRendererScale: rendererScale, + diskmapScale, + targetDiskmapScale: diskmapScale, + readsByPath, + diskify, + maxLcn, + diskmapTranslate: 0, + targetDiskmapTranslate: 0, + rendererTranslate: 0, + targetRendererTranslate: 0, + mouseX: 0, + mouseY: 0, + timelineIndicators: [], + lcnReads: [], + lastHoveredRect: null, + selectedEntry: null, + activePath: null, + topMarkerCoordinate: 0, + bottomMarkerCoordinate: markersCanvas.height, + isRefocused: isRefocused, + minTimeRelative: absTimeToRelTime(minTime), + maxTimeRelative: absTimeToRelTime(maxTime), }; + gOuterState.constrainedTimeBegin = minTime; + gOuterState.constrainedTimeEnd = maxTime; + gOuterState.constrainedTotalTime = totalTime; + renderer.scale(trackWidth, rendererScale); renderer.translate(0, 0); renderer.clearAll(); - drawBackground(); - drawForeground(); + drawBackground(isRefocused); + drawForeground(isRefocused); + drawRangeMarkers(); renderer.draw(); drawTopPathsInfo(); scheduleRedrawDiskmap(); } -function drawBackground() { + + + +function drawBackground(isRefocused, isZoom) { let { trackWidth, minTime, @@ -380,7 +426,9 @@ function drawBackground() { tracks, totalTime, rendererTranslate, - rendererScale + rendererScale, + minTimeRelative, + maxTimeRelative, } = gState; let timelineScale = @@ -388,11 +436,22 @@ function drawBackground() { rendererScale < 10000 ? 0.1 : rendererScale < 100000 ? 0.01 : 0.001; + + // When refocused, the upper bound may render it so that only a portion of + // the top rects should be shown. To account for that, we need to start + // at a height below 0. + let initialHeight; + if (isRefocused) { + initialHeight = minTimeRelative - Math.floor(minTimeRelative); + } else { + initialHeight = 0; + } + for (let i = 0; i < Math.ceil(totalTime / timelineScale); i++) { let color = (i & 1) ? "#ffffff" : "#efefef"; renderer.pushRect(color, 0, - timelineScale * i, + timelineScale * i - initialHeight, tracks.length, timelineScale, BACKGROUND_DEPTH - 0.05); @@ -413,23 +472,64 @@ function drawBackground() { gState.timelineIndicators = []; timeline.textContent = ""; - let printSeconds = timelineScale == 1; - for (let i = 0; i < Math.floor(totalTime / timelineScale); i++) { - let offset = i * timelineScale; + let offset; + for (let i = 0; i < Math.ceil(totalTime / timelineScale); i++) { + offset = i * timelineScale - initialHeight; let offsetPx = (offset + rendererTranslate) * rendererScale; - if (offsetPx < -VIEWPORT_BUFFER || offsetPx > canvas.height + VIEWPORT_BUFFER) { - continue; - } let div = document.createElement("div"); div.style.position = "fixed"; - div.style.left = `${canvas.width - 64}px`; + div.style.left = `${canvas.width - 40}px`; div.style.top = `${offsetPx}px`; - div.textContent = timelineScale == 1 ? `${i}s` : `${Math.round(i * timelineScale * 1000)}ms`; + div.textContent = timelineScale == 1 ? `${i + Math.floor(minTimeRelative)}s` : `${Math.round((i + Math.ceil(minTimeRelative)) * timelineScale * 1000)}ms`; timeline.appendChild(div); gState.timelineIndicators.push({div, offset}); } + + // Add the values of the range markers + if (isRefocused) { + let upperRangeDiv = document.createElement("div"); + let offsetUpperPx = (rendererTranslate) * rendererScale; + upperRangeDiv.style.position = "fixed"; + upperRangeDiv.style.left = `${canvas.width - 80}px`; + upperRangeDiv.style.top = `${offsetUpperPx}px`; + upperRangeDiv.textContent = timelineScale == 1 ? `${minTimeRelative.toFixed(2)}s` : `${(minTimeRelative * timelineScale * 1000).toFixed(2)}ms`; + timeline.appendChild(upperRangeDiv); + gState.timelineIndicators.push({upperRangeDiv, offset:0}); + + let lowerRangeDiv = document.createElement("div"); + let offsetLowerPx = (offset + rendererTranslate) * rendererScale; + lowerRangeDiv.style.position = "fixed"; + lowerRangeDiv.style.left = `${canvas.width - 80}px`; + lowerRangeDiv.style.top = `${canvas.height}px`; + lowerRangeDiv.textContent = timelineScale == 1 ? `${maxTimeRelative.toFixed(2)}s` : `${(maxTimeRelative * timelineScale * 1000).toFixed(2)}ms`; + timeline.appendChild(lowerRangeDiv); + gState.timelineIndicators.push({lowerRangeDiv, offset}); + } +} + + +function drawRangeMarkers() { + if (markerCtx) { + markerCtx.clearRect(0,0,markersCanvas.width, markersCanvas.height); + } else { + markerCtx = markersCanvas.getContext('2d'); + } + + var cw = markersCanvas.width; + var ch = markersCanvas.height; + reOffset(); + topRange = makeRangeControl(0,0,cw,ch, true); + bottomRange = makeRangeControl(0,0,cw,ch, false) + drawRangeControl(topRange, true); + drawRangeControl(bottomRange, false); + + + markerCtx.onmousedown=(function(e){handleMouseDown(e);}); + markerCtx.onmousemove=(function(e){handleMouseMove(e);}); + markerCtx.onmouseup=(function(e){handleMouseUpOut(e);}); + markerCtx.onmouseout=(function(e){handleMouseUpOut(e);}); } // From MDN (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) @@ -437,9 +537,8 @@ function escapeRegExp(string) { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); } -function drawForeground() { +function drawForeground(isRefocused) { let { - trackWidth, minTime, maxTime, tracks, @@ -449,6 +548,10 @@ function drawForeground() { selectedEntry, } = gState; + let { + constrainedTimeBegin + } = gOuterState; + let searchText = selectedEntry ? selectedEntry.path : searchbar.value; let searchRegex = new RegExp(escapeRegExp(searchText), "i"); @@ -474,17 +577,26 @@ function drawForeground() { entry.hiddenBySearch = !matchesSearch; - let startRelative = entry.start - minTime; - let endRelative = entry.end - minTime; - let startPixels = (startRelative + rendererTranslate) * rendererScale; - let endPixels = (endRelative + rendererTranslate) * rendererScale; + let startRelative = absTimeToRelTime(entry.start); + let endRelative = absTimeToRelTime(entry.end); + + let constrainedRelative, startPixels, endPixels; + if (isRefocused) { + constrainedRelative = absTimeToRelTime(constrainedTimeBegin); + startPixels = relTimeToScreenSpace(startRelative) - relTimeToScreenSpace(constrainedRelative); + endPixels = relTimeToScreenSpace(endRelative) - relTimeToScreenSpace(constrainedRelative); + } else { + startPixels = relTimeToScreenSpace(startRelative); + endPixels = relTimeToScreenSpace(endRelative); + constrainedRelative = 0; + } if (endPixels < -VIEWPORT_BUFFER || startPixels > canvas.height + VIEWPORT_BUFFER) { continue; } entry.rectHandle = renderer.pushRect(getColor(entry), - i, startRelative, + i, startRelative - constrainedRelative, 1, endRelative - startRelative, FOREGROUND_DEPTH, matchesSearch ? 1.0 : FILTERED_OUT_ENTRY_FILL); @@ -498,15 +610,13 @@ async function readFileContents() { let reader = new FileReader(); let text = await getFileText(reader, file); - let diskifyData = null; + diskifyData = null; if (diskifyInput.files[0]) { let diskifyText = await getFileText(reader, diskifyInput.files[0]); diskifyData = parseDiskify(diskifyText); } - let data; - let filename = file.name.toLowerCase(); if (filename.endsWith(".csv")) { data = parseCSV(text).map(row => Object.entries(row).reduce((acc,[key,val]) => { @@ -540,7 +650,7 @@ async function readFileContents() { return { operation, path, pid, tid, start, duration, detail, processName, stack, }; - }).filter(row => row.duration > 0 || row.operation == "Process Start"); + }).filter(row => (row.duration > 0 || row.operation == "Process Start") && row.processName == "firefox.exe"); let profilerData = null; if (profilerInput.files[0]) { @@ -562,7 +672,7 @@ async function readFileContents() { data.sort((lhs, rhs) => lhs.start - rhs.start); - await drawData(data, diskifyData); + await drawData(data, diskifyData, false); } }; @@ -637,8 +747,13 @@ function doScroll(dy) { rendererTranslate, targetRendererTranslate, mouseY, + isRefocused, } = gState; + if (isRefocused) { + return; + } + let totalTime = maxTime - minTime; let windowHeightInSeconds = canvas.height / rendererScale; let newTranslate = targetRendererTranslate - dy / rendererScale; @@ -682,9 +797,15 @@ function doZoom(scaleFactor) { maxTime, targetRendererScale, rendererTranslate, - mouseY + mouseY, + isRefocused, } = gState; + + if (isRefocused) { + return; + } + let windowTopInPixels = -rendererTranslate * targetRendererScale; let mousePositionAbsolute = windowTopInPixels + mouseY; let newMousePositionAbsolute = scaleFactor * mousePositionAbsolute; @@ -917,19 +1038,46 @@ function handleMouseMove(e) { } else if (isForDiskmap) { doDiskmapScroll(-dy); } + isDownTopSlider = false; + isDownBottomSlider = false; } else if (x < canvas.width) { let entry = getHoveredEntry(); showEntryTooltip(entry, {x, y}); + isDownTopSlider = false; + isDownBottomSlider = false; } else if (x - canvas.width < fsmapCanvas.width && gState.selectedEntry) { let entry = getHoveredReadEntry(); highlightEntry(entry); showEntryTooltip(entry); //, "Map of File reads (top is beginning of file, bottom is end, green are early reads, red are late)"); + isDownTopSlider = false; + isDownBottomSlider = false; } else if (x - canvas.width - fsmapCanvas.width < diskmapCanvas.width) { - let entry = getHoveredDiskmapEntry(); - highlightEntry(entry); - showEntryTooltip(entry); //, "Map of File reads by physical location on disk (green are early reads, red are late)"); + if (!isDownBottomSlider && !isDownTopSlider) { + let entry = getHoveredDiskmapEntry(); + highlightEntry(entry); + showEntryTooltip(entry); //, "Map of File reads by physical location on disk (green are early reads, red are late)"); + } else { + if (isDownTopSlider) { + gOuterState.constrainedTimeBegin = screenSpaceToAbsTime(gState.mouseY + absTimeToScreenSpace(gState.minTime), true); + + topRange.pct=Math.max(0,Math.min(1,(gState.mouseY-topRange.y)/topRange.height)); + markerCtx.clearRect(0,0,markersCanvas.width, gOuterState.bottomMarkerCoordinate-10); + + drawRangeControl(topRange, true); + } else if (isDownBottomSlider) { + gOuterState.constrainedTimeEnd = screenSpaceToAbsTime(gState.mouseY + absTimeToScreenSpace(gState.minTime), false); + + bottomRange.pct=Math.max(0,Math.min(1,(gState.mouseY-bottomRange.y)/bottomRange.height)); + markerCtx.clearRect(0,gOuterState.topMarkerCoordinate+10,markersCanvas.width, markersCanvas.height); + + drawRangeControl(bottomRange, false); + } + greyOutSelection(isDownTopSlider); + } } else { tooltip.style.display = "none"; + isDownTopSlider = false; + isDownBottomSlider = false; } } @@ -1075,6 +1223,68 @@ function drawPathInfo() { } } +function makeRangeControl(x, y, width, height, isTop) { + var range={x:x, y:y, width:width, height:height}; + range.x1 = range.x; + range.y1 = range.y + range.height; + if (isTop) { + range.pct=0; + } else { + range.pct=1; + } + return (range); +} + +function drawRangeControl(range, isTop){ + markerCtx.lineWidth=10; + markerCtx.beginPath(); + let thumbY=range.y+range.height*range.pct; + if (isTop) { + markerCtx.moveTo(range.width,thumbY-5); + markerCtx.lineTo(range.width,thumbY+5); + gOuterState.topMarkerCoordinate = thumbY; + markerCtx.lineTo(0, gOuterState.topMarkerCoordinate); + } else { + markerCtx.moveTo(range.width,thumbY+5); + markerCtx.lineTo(range.width,thumbY-5); + gOuterState.bottomMarkerCoordinate = thumbY; + markerCtx.lineTo(0, gOuterState.bottomMarkerCoordinate); + } + markerCtx.fill() +} + +function greyOutSelection(isTopSlider) { + let { + minTime, + maxTime, + tracks, + totalTime, + } = gState; + let { + constrainedTimeBegin, + constrainedTimeEnd, + } = gOuterState; + + renderer.clearGrey(isTopSlider); + + let unselectedRectangle; + if (isTopSlider) { + unselectedRectangle = renderer.pushRect(TIMELINE_UNSELECTED_GREY_TOP, + 0, 0, + tracks.length, absTimeToRelTime(constrainedTimeBegin) - absTimeToRelTime(minTime), + TIMELINE_SELECTION_OVERLAY_DEPTH, + 1, true); + } else { + unselectedRectangle = renderer.pushRect(TIMELINE_UNSELECTED_GREY_BOTTOM, + 0, absTimeToRelTime(constrainedTimeEnd) - absTimeToRelTime(minTime), + tracks.length, canvas.height, + TIMELINE_SELECTION_OVERLAY_DEPTH, + 1); + } + renderer.maybeMutateRect(unselectedRectangle, 1); + renderer.draw(); +} + function vcnRangeToLcnRanges(diskifyEntries, startVcn, vcnLength) { let curVcn = 0; let lcnRanges = []; @@ -1181,6 +1391,12 @@ function drawDiskmap() { diskmapRenderer.draw(); } +function reOffset() { + var BB = markersCanvas.getBoundingClientRect(); + offsetX=BB.left; + offsetY=BB.top; +} + function getHoveredDiskmapEntry() { let { mouseY, @@ -1303,13 +1519,18 @@ function smoothValueChange(key, currentValue, target, acceleration, callback) { let drawForegroundTimeout = null; function doRedraw() { + markerCtx.clearRect(0,0,markersCanvas.width, markersCanvas.height); + + let { isRefocused } = gState + renderer.clearAll(); - drawBackground(); - drawForeground(); + drawBackground(isRefocused); + drawForeground(isRefocused); renderer.draw(); drawForegroundTimeout = null; } + function scheduleRedraw() { if (drawForegroundTimeout) { return; @@ -1420,6 +1641,11 @@ function handleMouseDown(event) { scheduleRedrawDiskmap(); doRedraw(); } + } else if (event.which == 1 || event.button == 0) { + var mx=parseInt(event.clientX-offsetX); + var my=parseInt(event.clientY-offsetY); + isDownTopSlider = (my <= gOuterState.topMarkerCoordinate + 6 && my >= gOuterState.topMarkerCoordinate-6); + isDownBottomSlider = (my <= gOuterState.bottomMarkerCoordinate + 6 && my >= gOuterState.bottomMarkerCoordinate-6); } } } @@ -1429,12 +1655,78 @@ function handleMouseUp(event) { if (event.which == 2 || event.button == 4 ) { event.preventDefault(); gState.middleMouseDownFor = null; - } else { + } else if (event.explicitOriginalTarget.id == "redraw") { + event.preventDefault(); + refocusArea(); + } else if (event.explicitOriginalTarget.id == "reset") { + event.preventDefault(); + resetData(); + } else if (event.explicitOriginalTarget.id == "canvas-markers") { + event.preventDefault(); + isDownTopSlider = false; + isDownBottomSlider = false; + } + } +} +function handleMouseUpOut(event) { + event.preventDefault(); + isDownTopSlider = false; + isDownBottomSlider = false; +} + +/* + Focuses the canvas on the specified range. +*/ +function refocusArea() { + if (gState) { + let constrainedData = gOuterState.unconstrainedData.filter(row => + row.start > gOuterState.constrainedTimeBegin && row.start + row.duration < gOuterState.constrainedTimeEnd); + // TODO: constrain diskify data + if (constrainedData) { + //TO DO: this should have diskify data + drawData(constrainedData, null, true); } } } +/* + Returns the data back to the original view. +*/ +function resetData() { + if (gState) { + drawData(data, diskifyData, false); + } +} + +function absTimeToRelTime(absTime) { + let { minTime } = gOuterState; + return absTime - minTime; +} + +function relTimeToAbsTime(relTime) { + let { minTime } = gOuterState; + return relTime + minTime; +} + +function relTimeToScreenSpace(relTime) { + let { rendererScale, rendererTranslate } = gState; + return (relTime + rendererTranslate) * rendererScale; +} + +function absTimeToScreenSpace(absTime) { + return relTimeToScreenSpace(absTimeToRelTime(absTime)); +} + +function screenSpaceToRelTime(screenY) { + let { rendererScale, rendererTranslate } = gState; + return screenY / rendererScale - rendererTranslate; +} + +function screenSpaceToAbsTime(screenY, isTopSlider) { + return relTimeToAbsTime(screenSpaceToRelTime(screenY,isTopSlider)); +} + function handleSearchChange(event) { if (gState) { scheduleRedraw(); @@ -1467,6 +1759,8 @@ document.addEventListener("mousedown", handleMouseDown); document.addEventListener("mouseup", handleMouseUp); searchbar.addEventListener("keydown", handleSearchChange); colorBySelect.addEventListener("change", handleColorByChange); +refocus.addEventListener("click", refocusArea()); +reset.addEventListener("click", resetData()) renderer.startup(); fsmapRenderer.startup(); diff --git a/renderer.js b/renderer.js index 39b49bd..3bd2e30 100644 --- a/renderer.js +++ b/renderer.js @@ -215,6 +215,21 @@ function Renderer(canvas) { } } + function clearGrey(isTopSlider) { + generationId++; + for (let [cssColor, rectsObj] of Object.entries(rectsByColor)) { + if (cssColor == '#d6d5d2' && isTopSlider) { + rectsObj.vertexCount = 0; + rectsObj.hasAlpha = false; + rectsObj.isDirty = true; + } else if (cssColor == '#d6d5d1' && !isTopSlider) { + rectsObj.vertexCount = 0; + rectsObj.hasAlpha = false; + rectsObj.isDirty = true; + } + } + } + function scale(scaleX, scaleY) { worldScale = [scaleX, scaleY]; } @@ -332,6 +347,7 @@ function Renderer(canvas) { scale, translate, clearAll, + clearGrey, }; }