Closed phodal closed 10 months ago
Intellij IDEA file example: open-telemetry-metrics.2023-10-14-15-47-18.csv
OpenTelemetry Metrics
# OpenTelemetry Metrics report: .csv, 4 fields:
# <metric name>, <period start, nanoseconds>, <period end, nanoseconds>, <metric value>
# See CsvMetricsExporter for details.
#
# NAME, PERIOD_START_NANOS, PERIOD_END_NANOS, VALUE
FileNameCache.fastMisses,1697269638351000000,1697269698379000000,73315
FileNameCache.queries,1697269638351000000,1697269698379000000,3169844
FileNameCache.totalMisses,1697269638351000000,1697269698379000000,1064
JVM.totalCpuTimeMs,1697269638351000000,1697269698379000000,201428
OS.loadAverage,1697269638351000000,1697269698379000000,6.13427734375
JVM.usedNativeBytes,1697269638351000000,1697269698379000000,495786616
JVM.usedHeapBytes,1697269638351000000,1697269698379000000,1179651072
JVM.GC.collectionTimesMs,1697269638351000000,1697269698379000000,416
JVM.GC.collections,1697269638351000000,1697269698379000000,57
JVM.threadCount,1697269638351000000,1697269698379000000,187
Intellij IDEA: open-telemetry-metrics-plotter.html
<!--
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: {}
}
})
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 threadsCount = extractTimeSeries("JVM.threadCount")
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(
'Threads count',
'jvmBasics_Threads_Chart',
[{
name: 'threads count',
color: 'blue',
series: threadsCount
}],
/*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_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>
refs: https://github.com/babelcloud/LLM-RGB