![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/.cache/JetBrains/PhpStorm2024.1/log/ |
<!-- Page visualises *.csv files produced by CsvMetricsExporter -- i.e. OTel metrics exported in csv format Uses 'jquery' for DOM manipulation, 'plotly.js' for plotting, and SumoSelect for nice <select> UI (all under MIT license) To add a new 'predefined' chart -- add apt function to a PLOTTERS array. Use already added functions as an example --> <html> <head> <meta charset="utf-8"> <!-- license: MIT (https://github.com/jquery/jquery/blob/main/LICENSE.txt) --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" charset="utf-8"></script> <!-- license: MIT (https://github.com/plotly/plotly.js/blob/master/LICENSE) --> <script src="https://cdn.plot.ly/plotly-2.17.0.min.js" charset="utf-8"></script> <!-- Enhanced select with multi-option selection: (https://github.com/HemantNegi/jquery.sumoselect) (license: MIT) --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.sumoselect/3.4.9/jquery.sumoselect.min.js" charset="utf-8"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery.sumoselect/3.4.9/sumoselect.min.css"> <style lang="css"> body { font-family: Helvetica, serif; background-color: #F0F0F0; margin: 0; padding: 0; } /* ===== progress-bar details: =====*/ #progressBar { display: block; background-color: rgba(200, 200, 200, 0.8); position: absolute; top: 0; left: 0; bottom: 0; right: 0; cursor: progress; z-index: 1000; } #progressBar > div { width: 30em; height: 5em; position: absolute; top: 40%; left: 50%; margin-left: -15em; margin-top: -2.5em; font-size: 2em; padding: 0.5em; text-align: center; border-radius: 5px; border: 2px outset darkgrey; background-color: lightgrey; } #progressBar > div > div { float: left; clear: both; } /* ===== file-chooser elements: =====*/ .openFilesContainer { display: block; } .fileChooserForm { margin: 0; } /* Hide file chooser -- use it's <label> to trigger dialog */ .fileInput { display: none; } #fileInputLabel { display: block; font-size: 1em; padding-top: 0.5em; background: #ccc; cursor: pointer; border-radius: 5px; border: 1px solid #ccc; } #loadedFilesInfo { display: block; padding: 0.5em; margin: 0.5em; font-size: 1.2em; background: #ccc; border-radius: 5px; border: 1px solid #ccc; } /* ========= starting screen: ================== */ div.splashScreen { background-color: rgba(200, 200, 200, 0.7); display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; padding: 0; z-index: 100; font-size: 2em; } .splashScreen #fileChooserFormSplash { display: block; position: absolute; height: 3em; width: 40em; /*aka 'align-center hack' */ top: 30%; left: 50%; margin-top: -1.5em; margin-left: -20em; } .splashScreen #fileInputLabelSplash { display: block; padding: 1em; text-align: center; border: 1px outset darkgrey; border-radius: 5px; font-size: 1.2em; background: #ccc; cursor: pointer; } .splashScreen .faq { margin-top: 2em; font-size: 0.7em; background: #ccc; border: 1px outset darkgrey; border-radius: 5px; } .splashScreen .faq dl { padding: 0.5em; margin: 0; } .splashScreen .faq dl dt::before { content: "Q: "; font-weight: bold; } .splashScreen .faq dl dd { margin-bottom: 0.5em; margin-inline-start: 0; } .splashScreen .faq dl dd::before { content: "A: "; font-weight: bold; } /* ========= plots: ================== */ div.blockOfPlots { border: 2px outset lightgrey; border-radius: 0.5em; margin: 0.5em; } div.caption { font-weight: bold; font-size: 1.2em; font-family: Helvetica, serif; padding: 0.4em; cursor: pointer; background-color: lightblue; } /* open/close markers */ div > div.caption::before { content: '-'; font-family: monospaced, monospace; } div.hidden > div.caption::before { content: '+'; font-family: monospaced, monospace; } div.plot { /*border: 1px solid lightgrey;*/ margin: 0; } div.hidden > div.hideable { display: none; } /* ========== SumoSelect customization ========= */ .SumoSelect { width: 35em; font-size: 12pt; } </style> <!-- General objects/helpers: Point, TimeSeries, formatting methods --> <script lang="js"> function Point(time, value) { this.time = time this.value = value } Point.prototype = { time: null, value: null, toString() { return this.time + ", " + this.value } } function TimeSeries(points) { this.points = points.sort((p1, p2) => { const t1 = p1.time.getTime() const t2 = p2.time.getTime() if (t1 > t2) { return 1 } else if (t1 < t2) { return -1 } else { return 0 } }) const pointByTime = {} for (const p of points) { pointByTime[p.time.getTime()] = p } this.pointByTime = pointByTime } TimeSeries.prototype = { points: null, //Array(Point{time, value}) pointByTime: null, // Map{ time => Point(time, value) } toString() { return this.points.length + " points" }, length() { return this.points.length }, timestamps() { return this.points.map(p => p.time) }, values() { return this.points.map(p => p.value) }, combine(anotherTimeSeries, binaryOp) { const points = [] this.points.forEach(p => { const ap = anotherTimeSeries.pointByTime[p.time.getTime()] if (ap) { points.push(new Point(p.time, binaryOp(p.value, ap.value))) } else { points.push(new Point(p.time, binaryOp(p.value, 0))) } }) anotherTimeSeries.points.forEach(p => { const ap = this.pointByTime[p.time.getTime()] if (!ap) { points.push(new Point(p.time, binaryOp(0, p.value))) } }) return new TimeSeries(points) }, plus(anotherTimeSeries) { return this.combine(anotherTimeSeries, function (a, b) { return a + b }) }, minus(anotherTimeSeries) { return this.combine(anotherTimeSeries, function (a, b) { return a - b }) }, mul(anotherTimeSeries) { return this.combine(anotherTimeSeries, function (a, b) { return a * b }) }, div(anotherTimeSeries) { return this.combine(anotherTimeSeries, function (a, b) { return a / b }) }, mulScalar(scalar) { return new TimeSeries(this.points.map(p => new Point(p.time, p.value * scalar))) }, cumSum() { let sum = 0 return new TimeSeries(this.points.map(p => { sum += p.value return new Point(p.time, sum) })) } } Object.defineProperty(Number.prototype, 'formatSizeAsHumanReadable', { value: function () { const units = [' b', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB'] const step = 1024 let size = this let i = 0 for (; i < units.length - 1; i++) { if (size < step) { break } else { size /= step } } return size.toFixed(1) + units[i] } }) </script> <!-- Plotting: templates, plotting functions, predefined charts --> <!-- --> <script lang="js"> document.loadedAndParsedData = { parsedCSV: null, //parseFileContents() -> Array[ {name:String, startedAt:Date, value:Float} ] dateRange: { //min/max of all (parsedCSV.startedAt) min: null, max: null }, names: [], //Array of {parsedCSV.name} } document.plots = [] const PLOTLY_TEMPLATE = Plotly.makeTemplate({ data: [{ type: 'scatter', mode: 'lines+markers', connectgaps: true, line: {width: 1.5}, marker: {size: 2}, }], layout: { showlegend: true, dragmode: 'pan', legend: {x: 1, y: 1, xanchor: 'right'}, margin: {l: 60, r: 20, t: 40, b: 40}, xaxis: {type: 'date'}, yaxis: {rangemode: 'tozero'} } }) const PLOTLY_CONFIG = { displayModeBar: true, responsive: true, displaylogo: false, modeBarButtonsToRemove: ['select2d', 'lasso2d'] } /* Extracts time series with name nameOfSeriesToPlot from loadedAndParsedData. * @return TimeSeries of Point(time: Date, value: value} */ function extractTimeSeries(nameOfSeriesToPlot, parsedCSV = document.loadedAndParsedData.parsedCSV) { return new TimeSeries( parsedCSV .filter(row => row.name === nameOfSeriesToPlot) .map(row => new Point(row.startedAt, row.value)) ) } /* @param caption: String, plot title * @param domElementOrId container to plot into: id, DOM element, or jQuery object * @param dataToPlot: [{name, color, series, visible?}] * @param yaxis: optional {range, ...} */ function plotTimeSeries(caption, domElementOrId, dataToPlot, yaxis) { let domElement if (domElementOrId instanceof jQuery) { domElement = domElementOrId[0] } else if (typeof (domElementOrId) == 'string') { domElement = $("#" + domElementOrId)[0] } else if (domElementOrId.id) { domElement = $(domElementOrId)[0] } else { throw `Unrecognized ${domElementOrId}: should be (ID | DOM element | jQuery object)` } console.log(`plotTimeSeries(${caption}, ${domElement.id}, ${dataToPlot.length} plots, ...)`) //RC: seems like Plotly purge previous plot by itself anyway // if (element.plot) { // Plotly.purge(canvasElement); // } const plotlyTrace = dataToPlot.map(row => { const trace = { name: row.name, line: {color: row.color}, marker: {color: row.color}, x: row.series.timestamps(), y: row.series.values() } if (row.visible === 'legendonly') { trace.visible = 'legendonly' } return trace }) const layout = { template: PLOTLY_TEMPLATE, title: caption, xaxis: {range: document.loadedAndParsedData.dateRange} } if (typeof (yaxis) !== 'undefined') { layout.yaxis = yaxis } const plot = Plotly.newPlot( domElement, plotlyTrace, layout, PLOTLY_CONFIG ) //'synchronize' plots x-axes so that all plots show the same datetime range: domElement.on('plotly_relayout', (eventData) => { const from = eventData['xaxis.range[0]'] const to = eventData['xaxis.range[1]'] $(document.body).css('cursor', 'progress') try { if (from && to) { console.log(`${plot.id}: x-axis range: [${from}, ${to}]`) for (otherPlot of document.plots) { if (otherPlot !== plot) { console.log(`\treLayout: ${otherPlot.id}`) Plotly.relayout(otherPlot, {'xaxis.range': [from, to]}) } } } } finally { $(document.body).css('cursor', 'default') } }) domElement.plot = plot if (!document.plots.includes(domElement)) { document.plots.push(domElement) } return domElement } //==== Plots for specific subsystem, tailored and customized: ============ function plotBasicJVMCharts() { if (!document.loadedAndParsedData.parsedCSV) { console.log("Error: .loadedAndParsedData is not loaded") return } const usedHeapBytes = extractTimeSeries("JVM.usedHeapBytes") const maxHeapBytes = extractTimeSeries("JVM.maxHeapBytes") const usedNativeBytes = extractTimeSeries("JVM.usedNativeBytes") const maxNativeBytes = extractTimeSeries("JVM.maxNativeBytes") const totalDirectByteBuffersBytes = extractTimeSeries("JVM.totalDirectByteBuffersBytes") const threadsCount = extractTimeSeries("JVM.threadCount") const maxThreadsCount = extractTimeSeries("JVM.maxThreadCount") const newThreadsCount = extractTimeSeries("JVM.newThreadsCount") const gcCollections = extractTimeSeries("JVM.GC.collections") const gcTimesMs = extractTimeSeries("JVM.GC.collectionTimesMs") const totalBytesAllocated = extractTimeSeries("JVM.totalBytesAllocated") const totalCpuTimesMs = extractTimeSeries("JVM.totalCpuTimeMs") const osLoadAverage = extractTimeSeries("OS.loadAverage") console.log("JVM events: " + usedHeapBytes.length()) plotTimeSeries( 'Heap memory use', 'jvmBasics_Heap_Chart', [{ name: 'Used heap, Mb', color: 'green', series: usedHeapBytes.mulScalar(1.0 / 1024 / 1024) }, { name: 'Max heap, Mb', color: 'red', series: maxHeapBytes.mulScalar(1.0 / 1024 / 1024) }], /*yaxis:*/ { title: 'Mb' } ) const nativeMemoryNotLimited = maxNativeBytes.values().every(value => value <= 0) if (nativeMemoryNotLimited) { plotTimeSeries( 'Off-Heap (native) memory use', 'jvmBasics_OffHeap_Chart', [{ name: 'Used native, Mb', color: 'green', series: usedNativeBytes.mulScalar(1.0 / 1024 / 1024) }], /*yaxis:*/ { title: 'Mb' } ) } else { plotTimeSeries( 'Off-Heap (native) memory use', 'jvmBasics_OffHeap_Chart', [{ name: 'Used native, Mb', color: 'green', series: usedNativeBytes.mulScalar(1.0 / 1024 / 1024) }, { name: 'Max native, Mb', color: 'red', series: maxNativeBytes.mulScalar(1.0 / 1024 / 1024) }], /*yaxis:*/ { title: 'Mb' } ) } plotTimeSeries( 'Direct ByteBuffers', 'jvmBasics_DirectByteBuffers_Chart', [{ name: 'Total DirectByteBuffers, Mb', color: 'green', series: totalDirectByteBuffersBytes.mulScalar(1.0 / 1024 / 1024) }], /*yaxis:*/ { title: 'Mb' } ) plotTimeSeries( 'Threads count', 'jvmBasics_Threads_Chart', [{ name: 'current threads count', color: 'blue', series: threadsCount }, { name: 'max threads count', color: 'red', series: maxThreadsCount }, { name: 'new threads started', color: 'orange', series: newThreadsCount }], /*yaxis:*/ { title: 'threads' } ) plotTimeSeries( 'GC times', 'jvmBasics_GC_Times_Chart', [{ name: 'GC collections time, ms', color: 'blue', series: gcTimesMs }], /*yaxis:*/ { title: 'ms' } ) plotTimeSeries( 'Allocations', 'jvmBasics_Allocations_Chart', [{ name: 'Allocations', color: 'red', series: totalBytesAllocated.mulScalar(1.0/1024/1024/1024) }], /*yaxis:*/ { title: 'Gb' } ) plotTimeSeries( 'OS load average', 'jvmBasics_OS_LoadAvg_Chart', [{ name: 'OS load average, %', color: 'blue', series: osLoadAverage.mulScalar(100) }], /*yaxis:*/ { title: '%' } ) plotTimeSeries( 'JVM CPU Time', 'jvmBasics_JVM_CPUTime_Chart', [{ name: 'JVM CPU time, sec', color: 'blue', series: totalCpuTimesMs.mulScalar(1.0 / 1000) }], /*yaxis:*/ { title: 'sec' } ) } function plotAWTQueueCharts() { if (!document.loadedAndParsedData.parsedCSV) { console.log("Error: .loadedAndParsedData is not loaded") return } const awtEventsCount = extractTimeSeries("AWTEventQueue.eventsDispatched") const awtDispatchTimeMaxMs = extractTimeSeries("AWTEventQueue.dispatchTimeMaxNs").mulScalar(1e-6 /* ns-> ms */) const awtDispatchTime90PMs = extractTimeSeries("AWTEventQueue.dispatchTime90PNs").mulScalar(1e-6 /* ns-> ms */) const awtDispatchTimeAvgMs = extractTimeSeries("AWTEventQueue.dispatchTimeAvgNs").mulScalar(1e-6 /* ns-> ms */) console.log("AWT events: " + awtEventsCount) console.log("AWT dispatch time avg: " + awtDispatchTimeAvgMs) plotTimeSeries( 'AWT event queue: events count', 'EDT_awtEventsDispatchedChart', [{ name: 'events dispatched', color: 'blue', series: awtEventsCount }], /*yaxis:*/ { title: 'events' } ) plotTimeSeries( 'AWT event queue: event dispatching times', 'EDT_awtEventsTimingsChart', [{ name: 'avg, ms', color: 'green', series: awtDispatchTimeAvgMs }, { name: '90%, ms', color: 'orange', series: awtDispatchTime90PMs }, { name: 'MAX, ms', color: 'red', series: awtDispatchTimeMaxMs, visible: 'legendonly' //MAX is too dominating => toggle off by default }], /*yaxis:*/ { title: 'ms' } ) } function plotFlushQueueCharts() { if (!document.loadedAndParsedData.parsedCSV) { console.log("Error: .loadedAndParsedData is not loaded") return } //FlushQueue: ======== const flushQueueTasksExecuted = extractTimeSeries("FlushQueue.tasksExecuted") const flushQueueSizeAvg = extractTimeSeries("FlushQueue.queueSizeAvg") const flushQueueSizeMax = extractTimeSeries("FlushQueue.queueSizeMax") const flushQueueSize90P = extractTimeSeries("FlushQueue.queueSize90P") const flushQueueWaitingTimeMaxMs = extractTimeSeries("FlushQueue.waitingTimeMaxNs").mulScalar(1e-6) const flushQueueWaitingTime90PMs = extractTimeSeries("FlushQueue.waitingTime90PNs").mulScalar(1e-6) const flushQueueWaitingTimeAvgMs = extractTimeSeries("FlushQueue.waitingTimeAvgNs").mulScalar(1e-6) const flushQueueExecutionTimeMaxMs = extractTimeSeries("FlushQueue.executionTimeMaxNs").mulScalar(1e-6) const flushQueueExecutionTime90PMs = extractTimeSeries("FlushQueue.executionTime90PNs").mulScalar(1e-6) const flushQueueExecutionTimeAvgMs = extractTimeSeries("FlushQueue.executionTimeAvgNs").mulScalar(1e-6) plotTimeSeries( 'FlushQueue: events count', 'FlushQueue_tasksExecutedChart', [{ name: 'events dispatched', color: 'blue', series: flushQueueTasksExecuted }], /*yaxis:*/ { title: 'events', autorange: true } ) plotTimeSeries( 'FlushQueue: waiting times (ms)', 'FlushQueue_tasksWaitingTimesChart', [{ name: 'avg, ms', color: 'green', series: flushQueueWaitingTimeAvgMs }, { name: '90%, ms', color: 'orange', series: flushQueueWaitingTime90PMs }, { name: 'MAX, ms', color: 'red', series: flushQueueWaitingTimeMaxMs, visible: 'legendonly' //MAX is too dominating => toggle off by default }], /*yaxis:*/ { title: 'ms' } ) plotTimeSeries( 'FlushQueue: execution times (ms)', 'FlushQueue_tasksExecutionTimesChart', [{ name: 'avg, ms', color: 'green', series: flushQueueExecutionTimeAvgMs }, { name: '90%, ms', color: 'orange', series: flushQueueExecutionTime90PMs }, { name: 'MAX, ms', color: 'red', series: flushQueueExecutionTimeMaxMs, visible: 'legendonly' //MAX is too dominating => toggle them off by default }], /*yaxis:*/ { title: 'ms' } ) } function plotReadWriteActionsChart() { if (!document.loadedAndParsedData.parsedCSV) { console.log("Error: .loadedAndParsedData is not loaded") return } const writeActionExecutionsCount = extractTimeSeries("WriteAction.executionsCount") const readActionExecutionsCount = extractTimeSeries("ReadAction.executionsCount") const finalizedExecutionsCount = extractTimeSeries("NonBlockingReadAction.finalizedExecutionsCount") const failedExecutionsCount = extractTimeSeries("NonBlockingReadAction.failedExecutionsCount") const finalizedExecutionTimeMs = extractTimeSeries("NonBlockingReadAction.finalizedExecutionTimeUs").mulScalar(1e-3) const failedExecutionTimeMs = extractTimeSeries("NonBlockingReadAction.failedExecutionTimeUs").mulScalar(1e-3) plotTimeSeries( 'Read and Write Actions count', 'ReadAndWriteActions_CountChart', [{ name: 'Write Actions', color: 'red', series: writeActionExecutionsCount }, { name: 'Read Actions', color: 'blue', series: readActionExecutionsCount }], /*yaxis:*/ { title: 'actions' } ) plotTimeSeries( 'NonBlockingReadActions count: successful/interrupted', 'NonBlockingReads_CountChart', [{ name: 'successful', color: 'green', series: finalizedExecutionsCount }, { name: 'failed/interrupted', color: 'red', series: failedExecutionsCount }], /*yaxis:*/ { title: 'events' } ) plotTimeSeries( 'NonBlockingReadActions times: useful/wasted', 'NonBlockingReads_TimesChart', [{ name: 'useful (succeeded) time, ms', color: 'green', series: finalizedExecutionTimeMs }, { name: 'wasted (interrupted) time, ms', color: 'red', series: failedExecutionTimeMs }], /*yaxis:*/ { title: 'ms' } ) } function plotIndexesCharts() { if (!document.loadedAndParsedData.parsedCSV) { console.log("Error: .loadedAndParsedData is not loaded") return } const allKeysLookups = extractTimeSeries("Indexes.allKeys.lookups") const allKeysLookupsAvgMs = extractTimeSeries("Indexes.allKeys.lookupDurationAvgMs") const allKeysLookups90PMs = extractTimeSeries("Indexes.allKeys.lookupDuration90PMs") const allKeysLookupsMaxMs = extractTimeSeries("Indexes.allKeys.lookupDurationMaxMs") const stubIndexLookups = extractTimeSeries("Indexes.stubs.lookups") const stubIndexLookupsAvgMs = extractTimeSeries("Indexes.stubs.lookupDurationAvgMs") const stubIndexLookups90PMs = extractTimeSeries("Indexes.stubs.lookupDuration90PMs") const stubIndexLookupsMaxMs = extractTimeSeries("Indexes.stubs.lookupDurationMaxMs") const entriesIndexLookups = extractTimeSeries("Indexes.entries.lookups") const entriesIndexLookupsAvgMs = extractTimeSeries("Indexes.entries.lookupDurationAvgMs") const entriesIndexLookups90PMs = extractTimeSeries("Indexes.entries.lookupDuration90PMs") const entriesIndexLookupsMaxMs = extractTimeSeries("Indexes.entries.lookupDurationMaxMs") console.log("Indexes events: " + stubIndexLookups.length()) const totalTimeSpentInIndexLookupsMs = allKeysLookups.mul(allKeysLookupsAvgMs) .plus(stubIndexLookups.mul(stubIndexLookupsAvgMs)) .plus(entriesIndexLookups.mul(entriesIndexLookupsAvgMs)) const maxIndexLookupDurationMs = allKeysLookupsMaxMs .combine(stubIndexLookupsMaxMs, Math.max) .combine(entriesIndexLookupsMaxMs, Math.max) plotTimeSeries( 'Indexes lookups count', 'indexes_Lookups_Chart', [{ name: 'Stub lookups', color: 'green', series: stubIndexLookups }, { name: 'Entries lookups', color: 'orange', series: entriesIndexLookups }, { name: 'All-keys lookups', color: 'blue', series: allKeysLookups }], /*yaxis:*/ { title: 'count' } ) plotTimeSeries( 'Indexes lookup duration: total(stub+entries+allKeys), and MAX(stub,entries,allKeys)', 'indexes_Durations_Chart', [{ name: 'total time spent, sec', color: 'green', series: totalTimeSpentInIndexLookupsMs.mulScalar(1 / 1000) //ms->sec }, { name: 'MAX, sec', color: 'red', series: maxIndexLookupDurationMs.mulScalar(1 / 1000) //ms->sec }], /*yaxis:*/ { title: 'sec' } ) } function plotFilePageCacheCharts() { if (!document.loadedAndParsedData.parsedCSV) { console.log("Error: .loadedAndParsedData is not loaded") return } const pageFastHits = extractTimeSeries("FilePageCache.pageFastCacheHits") const pageHits = extractTimeSeries("FilePageCache.pageHits") const pageLoads = extractTimeSeries("FilePageCache.pageLoads") const pageMisses = extractTimeSeries("FilePageCache.pageLoadsAboveSizeThreshold") const pageLoadsTimeUs = extractTimeSeries("FilePageCache.totalPageLoadsUs") const pageDisposalTimeUs = extractTimeSeries("FilePageCache.totalPageDisposalsUs") const bufferCacheHits = extractTimeSeries("DirectByteBufferAllocator.hits") const bufferCacheMisses = extractTimeSeries("DirectByteBufferAllocator.misses") const buffersReclaimed = extractTimeSeries("DirectByteBufferAllocator.reclaimed") const buffersDisposed = extractTimeSeries("DirectByteBufferAllocator.disposed") const totalSizeOfBuffersInCacheBytes = extractTimeSeries("DirectByteBufferAllocator.totalSizeOfBuffersCachedInBytes") const totalSizeOfBuffersAllocatedBytes = extractTimeSeries("DirectByteBufferAllocator.totalSizeOfBuffersAllocatedInBytes") console.log("FPC fast hits: " + pageFastHits) const totalPagesRequested = pageFastHits.plus(pageHits).plus(pageMisses).plus(pageLoads) const pageFastHitsPercent = pageFastHits.div(totalPagesRequested).mulScalar(100) const pageHitsPercent = pageHits.div(totalPagesRequested).mulScalar(100) const pageLoadsPercent = pageLoads.plus(pageMisses).div(totalPagesRequested).mulScalar(100) plotTimeSeries( 'FilePageCache: hits/misses/loads', $("#filePageCache_HitsMisses_Chart"), [ {name: 'Fast hits', color: 'green', series: pageFastHitsPercent}, {name: 'Regular hits', color: 'blue', series: pageHitsPercent}, {name: 'Misses (loads)', color: 'red', series: pageLoadsPercent}, ], /*yaxis: */ { title: '%', autorange: false, range: [0, 100] } ) plotTimeSeries( 'FilePageCache: loads/dispose times', $("#filePageCache_Times_Chart"), [ {name: 'Page load times, ms', color: 'green', series: pageLoadsTimeUs.mulScalar(1e-3 /*us->ms*/)}, {name: 'Page dispose times, ms', color: 'blue', series: pageDisposalTimeUs.mulScalar(1e-3 /*us->ms*/)} ], /*yaxis: */ { title: 'ms', autorange: true } ) //DirectBufferAllocator charts: plotTimeSeries( 'DirectBufferAllocator: caching stats', $("#directBufferAllocator_Counts_Chart"), [ {name: 'Cache hits', color: 'green', series: bufferCacheHits}, {name: 'Cache misses', color: 'red', series: bufferCacheMisses}, //RC: buffers disposed & re-used are quite niche -- turn them off by default: {name: 'Buffers disposed', color: 'yellow', series: buffersDisposed, visible: 'legendonly'}, {name: 'Buffers re-used', color: 'blue', series: buffersReclaimed, visible: 'legendonly'}, ], /*yaxis: */ { title: '', autorange: true } ) plotTimeSeries( 'DirectBufferAllocator: native memory usage', $("#directBufferAllocator_Bytes_Chart"), [ {name: 'ByteBuffers allocated, Mb', color: 'red', series: totalSizeOfBuffersAllocatedBytes.mulScalar(1 / 1024 / 1024)}, {name: 'ByteBuffers in cache, Mb', color: 'green', series: totalSizeOfBuffersInCacheBytes.mulScalar(1 / 1024 / 1024)} ], /*yaxis: */ { title: 'Mb', autorange: true } ) } function plotFilePageCacheLockFreeCharts() { if (!document.loadedAndParsedData.parsedCSV) { console.log("Error: .loadedAndParsedData is not loaded") return } const totalNativeBytesAllocated = extractTimeSeries("FilePageCacheLockFree.totalNativeBytesAllocated") const totalNativeBytesReclaimed = extractTimeSeries("FilePageCacheLockFree.totalNativeBytesReclaimed") const totalHeapBytesAllocated = extractTimeSeries("FilePageCacheLockFree.totalHeapBytesAllocated") const totalHeapBytesReclaimed = extractTimeSeries("FilePageCacheLockFree.totalHeapBytesReclaimed") const nativeBytesInUse = extractTimeSeries("FilePageCacheLockFree.nativeBytesInUse") const heapBytesInUse = extractTimeSeries("FilePageCacheLockFree.heapBytesInUse") const totalPagesAllocated = extractTimeSeries("FilePageCacheLockFree.totalPagesAllocated") const totalPagesReclaimed = extractTimeSeries("FilePageCacheLockFree.totalPagesReclaimed") const totalPagesHandedOver = extractTimeSeries("FilePageCacheLockFree.totalPagesHandedOver") const totalPageAllocationsWaited = extractTimeSeries("FilePageCacheLockFree.totalPageAllocationsWaited") const totalPagesWritten = extractTimeSeries("FilePageCacheLockFree.totalPagesWritten") const totalPagesRequested = extractTimeSeries("FilePageCacheLockFree.totalPagesRequested") const totalBytesRequested = extractTimeSeries("FilePageCacheLockFree.totalBytesRequested") const totalBytesRead = extractTimeSeries("FilePageCacheLockFree.totalBytesRead") const totalBytesWritten = extractTimeSeries("FilePageCacheLockFree.totalBytesWritten") const totalPagesRequestsMs = extractTimeSeries("FilePageCacheLockFree.totalPagesRequestsMs") const totalPagesReadMs = extractTimeSeries("FilePageCacheLockFree.totalPagesReadMs") const totalPagesWriteMs = extractTimeSeries("FilePageCacheLockFree.totalPagesWriteMs") const housekeeperTurnsDone = extractTimeSeries("FilePageCacheLockFree.housekeeperTurnsDone") const housekeeperTurnsSkipped = extractTimeSeries("FilePageCacheLockFree.housekeeperTurnsSkipped") const housekeeperTimeSpentMs = extractTimeSeries("FilePageCacheLockFree.housekeeperTimeSpentMs") const totalClosedStoragesReclaimed = extractTimeSeries("FilePageCacheLockFree.totalClosedStoragesReclaimed") console.log("FPC native bytes allocated: " + totalNativeBytesAllocated) // const pageLoadsPercent = pageLoads.plus(pageMisses).div(totalPagesRequested).mulScalar(100) plotTimeSeries( 'Page count: requested, allocated, reclaimed...', $("#filePageCacheLockFree_PageCounts"), [ {name: 'Pages requested by app', color: 'green', series: totalPagesRequested, visible: 'legendonly'}, {name: 'Pages allocated', color: 'red', series: totalPagesAllocated}, {name: 'Pages reclaimed', color: 'blue', series: totalPagesReclaimed}, {name: 'Pages reused immediately', color: 'cyan', series: totalPagesHandedOver}, {name: 'Pages written on disk', color: 'magenta', series: totalPagesWritten}, {name: 'Pages allocation waited', color: 'orange', series: totalPageAllocationsWaited} ], /*yaxis: */ { title: '', autorange: true } ) plotTimeSeries( 'Page timings', $("#filePageCacheLockFree_PageTimings"), [ {name: 'Total time of page requests', color: 'blue', series: totalPagesRequestsMs.mulScalar(1e-3)}, {name: 'Total time of page reads', color: 'green', series: totalPagesReadMs.mulScalar(1e-3)}, {name: 'Total time of page writes', color: 'red', series: totalPagesWriteMs.mulScalar(1e-3)} ], /*yaxis: */ { title: 'sec', autorange: true } ) plotTimeSeries( 'Data flows', $("#filePageCacheLockFree_PageBytes"), [ { name: 'Total bytes requested by app', color: 'blue', series: totalBytesRequested.mulScalar(1e-6), visible: 'legendonly' },//usually not representative {name: 'Total bytes read', color: 'green', series: totalBytesRead.mulScalar(1e-6)}, {name: 'Total bytes written', color: 'red', series: totalBytesWritten.mulScalar(1e-6)} ], /*yaxis: */ { title: 'Mb', autorange: true } ) plotTimeSeries( 'Caching efficiency', $("#filePageCacheLockFree_Caching"), [ {name: 'Cache hits', color: 'green', series: totalPagesRequested.minus(totalPagesAllocated).div(totalPagesRequested).mulScalar(100) }, {name: 'Cache misses', color: 'red', series: totalPagesAllocated.div(totalPagesRequested).mulScalar(100) } ], /*yaxis: */ { title: '%', autorange: false, range: [0, 100] } ) //MAYBE RC: read/write _speeds_ feel to be more representative (i.e. to detect slow storage). But // in practice numbers are quite irregular -- likely because since OS-level caching plays // too big role. // plotTimeSeries( // 'Data flows', // $("#filePageCacheLockFree_PageBytes"), // [ // {name: 'Requested by app', color: 'blue', // series: totalBytesRequested.mulScalar(1e-6).div(totalPagesRequestsMs.mulScalar(1e-3)), // visible: 'legendonly'},//usually not representative // {name: 'Read', color: 'green', series: totalBytesRead.mulScalar(1e-6).div(totalPagesReadMs.mulScalar(1e-3))}, // {name: 'Written', color: 'red', series: totalBytesWritten.mulScalar(1e-6).div(totalPagesWriteMs.mulScalar(1e-3))} // ], // /*yaxis: */ { // title: 'Mb/sec', // autorange: true // } // ) plotTimeSeries( 'Memory used by page buffers: Heap vs Off-heap (Native)', $("#filePageCacheLockFree_HeapVsNativeMemoryUsed"), [ {name: 'Off-heap memory in use', color: 'blue', series: nativeBytesInUse.mulScalar(1e-6)}, {name: 'Heap memory in use', color: 'red', series: heapBytesInUse.mulScalar(1e-6)} ], /*yaxis: */ { title: 'Mb', autorange: true } ) plotTimeSeries( 'Housekeeper thread activity', $("#filePageCacheLockFree_Housekeeper"), [ {name: 'Turns done', color: 'green', series: housekeeperTurnsDone}, {name: 'Turns skipped', color: 'blue', series: housekeeperTurnsSkipped, visible: 'legendonly'} ], /*yaxis: */ { title: '', autorange: true } ) plotTimeSeries( 'Housekeeper thread times', $("#filePageCacheLockFree_HousekeeperTimes"), [ {name: 'Time spent', color: 'green', series: housekeeperTimeSpentMs.mulScalar(1e-3)} ], /*yaxis: */ { title: 'sec', autorange: true } ) } //====== 'Custom' plot: ================= /** namesOfSeriesToPlot: array of strings, names of time series in a document.loadedAndParsedData.parsedCSV */ function extractAndPlotCustomTimeSeries(namesOfSeriesToPlot) { if (!document.loadedAndParsedData.parsedCSV) { console.log("Error: .loadedAndParsedData is not loaded") return } const canvasElementToPlotOn = $("#customChartPlotly") if (!Array.isArray(namesOfSeriesToPlot)) { //legacy version: not an array, just a string namesOfSeriesToPlot = [namesOfSeriesToPlot] } const title = namesOfSeriesToPlot.join(', ') const dataToPlot = namesOfSeriesToPlot.map((timeSeriesName) => { const timeSeries = extractTimeSeries(timeSeriesName) if (!timeSeries) { console.log(`Error: .loadedAndParsedData.parsedCSV[${timeSeriesName}] is not exists`) console.log(document.loadedAndParsedData.parsedCSV) } return { name: timeSeriesName, series: timeSeries } }).filter((dataRow) => { return dataRow.series != null }) plotTimeSeries( title, canvasElementToPlotOn, dataToPlot ) } function plotCustomTimeSeries(title, timeSeries, canvasElement) { plotTimeSeries( title, canvasElement, [{ name: title, color: 'green', series: timeSeries }] ) } //================= List of all 'plotter' functions: ================= // Append newly created plotters here, to be automatically caught on file loading: const PLOTTERS = [ plotBasicJVMCharts, plotAWTQueueCharts, plotFlushQueueCharts, plotReadWriteActionsChart, plotIndexesCharts, plotFilePageCacheCharts, plotFilePageCacheLockFreeCharts ] </script> <!-- File(s) loading and binding all together: --> <script lang="js"> function readFiles(files) { console.log("Files: " + files.length) if (files.length === 0) { return } $("#splashScreen").hide() document.progressBar.show(`Parsing ${files.length} files...`) document.progressBar.update(0) for (plot of document.plots) { Plotly.purge(plot) } document.plots = [] const fileNames = Array.from(files) .reduce((string, file) => `${string + file.name} (${file.size.formatSizeAsHumanReadable()}) `, "") const totalFileSize = Array.from(files) .map(file => file.size) .reduce((total, size) => total + size, 0) const loadingChain = new Promise((resolve, reject) => { let fileContents = [] for (file of files) { const fileName = file.name const reader = new FileReader() reader.addEventListener('load', (event) => { const fileText = event.target.result console.log(`\tread ${fileName}: ${fileText.length} b`) fileContents.push(fileText) if (fileContents.length < files.length) { document.progressBar.update(fileContents.length * 40 / files.length) } else { //all files done: resolve(fileContents) } }) console.log(`reading ${fileName}...`) reader.readAsText(file) } }) .then(parseFileContents) .then(csvRows => { document.progressBar.update(69) document.loadedAndParsedData.parsedCSV = csvRows document.loadedAndParsedData.names = [...new Set(csvRows.map(row => { return row.name }))] let minTs = 1e30, maxTs = 0 csvRows.map(row => { return row.startedAt.getTime() }).forEach(timestamp => { minTs = Math.min(minTs, timestamp) maxTs = Math.max(maxTs, timestamp) }) document.loadedAndParsedData.dateRange = [new Date(minTs), new Date(maxTs)] $("#filesLoadedInfo") .html(`Loaded <b>${files.length} file(s)</b>: ${totalFileSize.formatSizeAsHumanReadable()}, <b>${csvRows.length}</b> points`) .attr('title', fileNames) $("#pointsLoadedInfo").html( `Interval covered: [${new Date(minTs).toLocaleString()} — ${new Date(maxTs).toLocaleString()}] (local TZ)` ) document.progressBar.show(`Parsed ${files.length} files, ${totalFileSize.formatSizeAsHumanReadable()}, plotting...`) document.progressBar.update(74) return csvRows }) .then(updateUIAfterDataLoaded) //Plot charts: const progressPerPlotter = (100 - 80 - 1) / PLOTTERS.length loadingChain.then(() => { document.progressBar.update(80) }) for (const plotter of PLOTTERS) { loadingChain .then(plotter) .then(() => { document.progressBar.update(document.progressBar.currentValue() + progressPerPlotter) }) } loadingChain.then(() => { document.progressBar.update(80) document.progressBar.hide() }) } /* @param contents: String, multi-lines csv * @return Array of records {name, startedAt:Date, value: float} */ function parseFileContents(contents) { console.log("File contents: " + contents.length) let data = [] for (const content of contents) { const lines = content.split('\n') for (const line of lines) { if (!line.startsWith('#') && line.trim().length > 0) { const parts = line.split(',') if (parts.length === 4) {//name, startEpochNs, endEpochNs, value: data.push({ name: parts[0].trim(), startedAt: new Date(parseInt(parts[1].trim()) / 1_000_000 /* ns -> ms */), value: parseFloat(parts[3].trim()) }) } else { console.log("Error parsing line: [" + line + "]") } } } } return data } /* Updates UI after CSV data is loaded */ function updateUIAfterDataLoaded(data) { const names = document.loadedAndParsedData.names //Enhance default html <select multi> with nice UI: const timeSeriesChooser = $("#timeSeriesChooser") const previouslySelectedValues = timeSeriesChooser.val() timeSeriesChooser.html("") for (const name of names.sort()) { timeSeriesChooser.append(`<option value="${name}">${name}</option>`) } if (timeSeriesChooser[0].sumo) { timeSeriesChooser[0].sumo.reload() for (const valueToSelect of previouslySelectedValues) { timeSeriesChooser[0].sumo.selectItem(valueToSelect) } } else { timeSeriesChooser.SumoSelect({ placeholder: 'Choose time series to plot...', max: 6, csvDispCount: 6, search: true // clearAll: true -- has some issues }) timeSeriesChooser[0].sumo.selectItem(0) } //TODO RC: Unfinished work: setup _time-range_ chooser -- so user could limit plots to subset // of datetime range covered in files. // (For now #timeRangeChooser is hidden to not distract users) const timeRangeChooser = $("#timeRangeChooser") timeRangeChooser.append(`<option value="">---all---</option>`) const min = document.loadedAndParsedData.dateRange[0] const max = document.loadedAndParsedData.dateRange[1] //RC how to iterate time range hour by hour? // for(v=min; v<max; v++) { // $timeRangeChooser.append(`<option value="${v}">${v}</option>`) // } } </script> <title>OTel.Metrics Plotter (*.csv)</title> </head> <body> <div id="progressBar" style="display: none"> <div> <div id="caption">LOADING...</div> <div><span id="percents">0</span>%</div> <div>(Please be patient: browser is working hard for your honor!)</div> </div> </div> <div id="splashScreen" class="splashScreen"> <form id="fileChooserFormSplash"> <label id="fileInputLabelSplash" for="fileInputSplash" autofocus class="fileInputLabel"> Drag & Drop <span style="font-family: monospace">open-telemetry-metrics.*.csv</span> file(s) on the page<br/> (or click here for file-open dialog) </label> <input type="file" id="fileInputSplash" multiple accept="text/csv" title="Select 'open-telemetry-metrics.*.csv' files" class="fileInput" onchange="readFiles(event.target.files)" /> <div class="faq hidden"> <div class="caption">FAQ (What is this?)</div> <div class="hideable"> <dl> <dt>What this page is for?</dt> <dd>During its work IDE exports its internal monitoring data into a <span style="font-family: monospace">open-telemetry-metrics.*.csv</span> files. The page could parse those files and plot nice time series charts. The data is mostly for JetBrains support and development engineers. </dd> <dt>Why do I need it?</dt> <dd>Maybe you don't need it. But it is quite easy to try and see yourself: drag-n-drop any <span style="font-family: monospace">open-telemetry-metrics.*.csv</span> file onto the page. </dd> <dt>There to find <span style="font-family: monospace">open-telemetry-metrics.*.csv</span> files? </dt> <dd>In IDE logs folder (menu: <span style="font-family: monospace">Help/Show logs in Finder</span>)</dd> </dl> </div> </div> </form> </div> <div id="loadedFilesInfo"> <div id="filesLoadedInfo"></div> <div id="pointsLoadedInfo"></div> <div class="openFilesContainer"> <form id="fileChooserForm" class="fileChooserForm"> <label for="timeRangeChooser" style="display: none">Reduce time range to specific hour:</label> <select id="timeRangeChooser" style="display: none"></select> <!-- class="fileInputLabel"--> <label id="fileInputLabel" for="fileInput" autofocus> To view another file(s): <b>Drag & Drop</b> <span style="font-family: monospace">*.csv</span> file(s) on the page, or <b>click</b> here to use file-choosing dialog. </label> <input type="file" id="fileInput" accept="text/csv" multiple class="fileInput" title="Select 'open-telemetry-metrics.*.csv' files" onchange="readFiles(event.target.files)" /> </form> </div> </div> <div id="plots"> <div id="jvmBasics" class="blockOfPlots"> <div class="caption">JVM: heap, native memory, threads, CPU</div> <div id="jvmBasics_Heap_Chart" class="plot hideable"></div> <div id="jvmBasics_OffHeap_Chart" class="plot hideable"></div> <div id="jvmBasics_DirectByteBuffers_Chart" class="plot hideable"></div> <div id="jvmBasics_Threads_Chart" class="plot hideable"></div> <div id="jvmBasics_GC_Times_Chart" class="plot hideable"></div> <div id="jvmBasics_Allocations_Chart" class="plot hideable"></div> <div id="jvmBasics_JVM_CPUTime_Chart" class="plot hideable"></div> <div id="jvmBasics_OS_LoadAvg_Chart" class="plot hideable"></div> </div> <div id="AWT_and_Flush_Queues" class="blockOfPlots"> <div class="caption">EDT: AWT & Flush Queues</div> <div id="EDT_awtEventsDispatchedChart" class="plot hideable"></div> <div id="EDT_awtEventsTimingsChart" class="plot hideable"></div> <!--<div class="caption">FlushQueue (tasks dispatching):</div>--> <div id="FlushQueue_tasksExecutedChart" class="plot hideable"></div> <div id="FlushQueue_tasksWaitingTimesChart" class="plot hideable"></div> <div id="FlushQueue_tasksExecutionTimesChart" class="plot hideable"></div> </div> <div id="Write_Read_Actions" class="blockOfPlots"> <div class="caption">Actions: Write, Read, and Non-Blocking-Read</div> <div id="ReadAndWriteActions_CountChart" class="plot hideable"></div> <div id="NonBlockingReads_CountChart" class="plot hideable"></div> <div id="NonBlockingReads_TimesChart" class="plot hideable"></div> </div> <div id="indexes" class="blockOfPlots"> <div class="caption">Indexes: lookups count & duration</div> <div id="indexes_Lookups_Chart" class="plot hideable"></div> <div id="indexes_Durations_Chart" class="plot hideable"></div> </div> <div id="filePageCache" class="blockOfPlots"> <div class="caption">FilePageCache:</div> <div id="filePageCache_HitsMisses_Chart" class="plot hideable"></div> <div id="filePageCache_Times_Chart" class="plot hideable"></div> <div id="directBufferAllocator_Counts_Chart" class="plot hideable"></div> <div id="directBufferAllocator_Bytes_Chart" class="plot hideable"></div> </div> <div id="filePageCacheLockFree" class="blockOfPlots"> <div class="caption">FilePageCache (New):</div> <div id="filePageCacheLockFree_PageCounts" class="plot hideable"></div> <div id="filePageCacheLockFree_PageTimings" class="plot hideable"></div> <div id="filePageCacheLockFree_PageBytes" class="plot hideable"></div> <div id="filePageCacheLockFree_Caching" class="plot hideable"></div> <div id="filePageCacheLockFree_HeapVsNativeMemoryUsed" class="plot hideable"></div> <div id="filePageCacheLockFree_Housekeeper" class="plot hideable"></div> <div id="filePageCacheLockFree_HousekeeperTimes" class="plot hideable"></div> </div> <div id="custom" class="blockOfPlots"> <div class="caption"> <label for="timeSeriesChooser">Plot other:</label> <select name="timeSeriesChooser" id="timeSeriesChooser" multiple onchange="extractAndPlotCustomTimeSeries($(this).val())"> </select> (no more than 6 time series at once) </div> <div id="customChartPlotly" class="plot hideable"></div> </div> </div> <script lang="js"> /* Make page accept drag-n-drop files */ initDnD = function (dragAndDropAreaElement, processFiles) { //area 'sensitive' to drag-n-drop: const dragAndDropArea = $(dragAndDropAreaElement) //create 'glass pane' for DnD visual signalling: const dragAndDropGlassPane = $('<div/>', { id: 'dragAndDropGlassPane', style: "position:absolute; top:0;bottom:0;left:0;right:0; z-index:1000;pointer-events:none;" }) dragAndDropGlassPane.appendTo(dragAndDropArea) dragAndDropGlassPane.hide() const dragAndDropAssistant = $("<div/>", { id: 'dragAndDropAssistant', style: 'position:absolute; z-index:1001; font-size: 2em; ' + 'margin-top:-1.5em; margin-left:-5em; ' + 'display:none; pointer-events:none;' + 'color: rgb(0, 100, 0);' }).text("Drop it. Right here. Now.") dragAndDropGlassPane.append(dragAndDropAssistant) dragAndDropGlassPane.showPanel = (event, readyToAcceptDrop) => { if (readyToAcceptDrop) { dragAndDropGlassPane.css("background-color", "rgba(200, 220, 200, 0.7)") } else { dragAndDropGlassPane.css("background-color", "rgba(220, 200, 200, 0.7)") } if (event && readyToAcceptDrop) { //show validating text under cursor: const x = event.clientX + 10 const y = event.clientY - 10 dragAndDropAssistant.css({left: x - 10, top: y}).show() } dragAndDropGlassPane.show() } dragAndDropGlassPane.hidePanel = () => { dragAndDropGlassPane.hide() } ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dragAndDropArea.on(eventName, (event) => { event.preventDefault() event.stopPropagation() //console.log(event.originalEvent) switch (event.type) { case "dragenter": case "dragover": dragAndDropGlassPane.showPanel(event, /*accept: */true) break case "dragleave": dragAndDropGlassPane.hidePanel() break case "drop": dragAndDropGlassPane.hidePanel() if (event.originalEvent.dataTransfer) { const dataTransfer = event.originalEvent.dataTransfer const files = dataTransfer.files if (files.length && files.length > 0) { processFiles(files) } } break } }) }) } initDnD(document.body, readFiles) initProgressBar = function () { const progressBarGlassPane = $("#progressBar") const textPane = $("#progressBar #caption") const percentsPane = $("#progressBar #percents") progressBarGlassPane.hide() let percentValue = 0 document.progressBar = { show: (caption) => { textPane.text(caption) percentsPane.text(percentValue.toFixed(0)) progressBarGlassPane.show() }, update: (percents) => { percentValue = percents percentsPane.text(percents.toFixed(0)) }, hide: () => { progressBarGlassPane.hide() }, currentValue: () => { return percentValue } } } initProgressBar() //setup panels close/open by clicking on the panel caption: $('.caption').on('click', (event) => { //ignore clicks propagated from elements _inside_ .caption -- those elements could // have their own use for mouse clicks, so don't interfere with them: if (event.currentTarget === event.target) { const parent = $(event.currentTarget.parentElement) parent.toggleClass('hidden') } }) </script> </body> </html>