From 738ea29969c2902815dcee5adf2f334b97dd9090 Mon Sep 17 00:00:00 2001 From: Julian P Samaroo Date: Thu, 4 Aug 2022 19:32:55 -0500 Subject: [PATCH] DaggerWebDash: UI and functionality improvements DaggerWebDash: Generalize for non-Dagger users ProcessorSaturation: Move from TimespanLogging to Dagger ProcessorSaturation: Avoid bad JSON serialization DaggerWebDash.GanttPlot: Remove timeline key DaggerWebDash: Add custom x-label, block coloring, and line disable opts DaggerWebDash: Show tooltip on hover --- benchmarks/benchmark.jl | 18 +-- docs/src/logging.md | 2 +- docs/src/scheduler-visualization.md | 2 +- lib/DaggerWebDash/src/core.jl | 3 +- lib/DaggerWebDash/src/d3.jl | 10 +- lib/DaggerWebDash/src/index.html | 207 ++++++++++++++++------------ lib/DaggerWebDash/test/runtests.jl | 4 +- lib/TimespanLogging/src/extras.jl | 24 ---- src/lib/logging-events.jl | 34 ++++- test/logging.jl | 2 +- 10 files changed, 172 insertions(+), 134 deletions(-) diff --git a/benchmarks/benchmark.jl b/benchmarks/benchmark.jl index 5a9c0485f..7ca08be61 100644 --- a/benchmarks/benchmark.jl +++ b/benchmarks/benchmark.jl @@ -160,15 +160,15 @@ function main() end elseif render == "webdash" || savelogs ml = TimespanLogging.MultiEventLog() - ml[:core] = TimespanLogging.CoreMetrics() - ml[:id] = TimespanLogging.IDMetrics() - # FIXME: ml[:timeline] = TimespanLogging.TimelineMetrics() - profile && (ml[:profile] = TimespanLoggingWebDash.ProfileMetrics()) - ml[:wsat] = TimespanLogging.WorkerSaturation() - ml[:loadavg] = TimespanLogging.CPULoadAverages() + ml[:core] = TimespanLogging.Events.CoreMetrics() + ml[:id] = TimespanLogging.Events.IDMetrics() + # FIXME: ml[:timeline] = TimespanLogging.Events.TimelineMetrics() + profile && (ml[:profile] = DaggerWebDash.ProfileMetrics()) + ml[:wsat] = Dagger.Events.WorkerSaturation() + ml[:loadavg] = TimespanLogging.Events.CPULoadAverages() ml[:bytes] = Dagger.Events.BytesAllocd() - ml[:mem] = TimespanLogging.MemoryFree() - ml[:esat] = TimespanLogging.EventSaturation() + ml[:mem] = TimespanLogging.Events.MemoryFree() + ml[:esat] = TimespanLogging.Events.EventSaturation() ml[:psat] = Dagger.Events.ProcessorSaturation() lw = TimespanLogging.Events.LogWindow(5*10^9, :core) logs_df = DataFrame([key=>[] for key in keys(ml.consumers)]...) @@ -178,7 +178,7 @@ function main() d3r = DaggerWebDash.D3Renderer(live_port; seek_store=ts) push!(lw.creation_handlers, d3r) push!(lw.deletion_handlers, d3r) - push!(d3r, GanttPlot(:core, :id, :timeline, :esat, :psat, "Overview")) + push!(d3r, GanttPlot(:core, :id, :esat, :psat; title="Overview")) # TODO: push!(d3r, ProfileViewer(:core, :profile, "Profile Viewer")) push!(d3r, LinePlot(:core, :wsat, "Worker Saturation", "Running Tasks")) push!(d3r, LinePlot(:core, :loadavg, "CPU Load Average", "Average Running Threads")) diff --git a/docs/src/logging.md b/docs/src/logging.md index 24303ca6f..951af63b1 100644 --- a/docs/src/logging.md +++ b/docs/src/logging.md @@ -77,9 +77,9 @@ TimespanLogging.Events.FullMetrics TimespanLogging.Events.CPULoadAverages TimespanLogging.Events.MemoryFree TimespanLogging.Events.EventSaturation -TimespanLogging.Events.WorkerSaturation Dagger.Events.BytesAllocd Dagger.Events.ProcessorSaturation +Dagger.Events.WorkerSaturation ``` The `MultiEventLog` also has a mechanism to call a set of functions, called diff --git a/docs/src/scheduler-visualization.md b/docs/src/scheduler-visualization.md index a20b44f8c..1696aac5d 100644 --- a/docs/src/scheduler-visualization.md +++ b/docs/src/scheduler-visualization.md @@ -40,7 +40,7 @@ d3r = DaggerWebDash.D3Renderer(8080) ## Add some plots! Rendered top-down in order # Show an overview of all generated events as a Gantt chart -push!(d3r, GanttPlot(:core, :id, :timeline, :esat, :psat, "Overview")) +push!(d3r, GanttPlot(:core, :id, :esat, :psat; title="Overview")) # Show various numerical events as line plots over time push!(d3r, LinePlot(:core, :wsat, "Worker Saturation", "Running Tasks")) diff --git a/lib/DaggerWebDash/src/core.jl b/lib/DaggerWebDash/src/core.jl index f22b57e61..c6ccd8f6c 100644 --- a/lib/DaggerWebDash/src/core.jl +++ b/lib/DaggerWebDash/src/core.jl @@ -3,8 +3,9 @@ using StructTypes, JSON3 sanitize(t::Tuple) = map(sanitize, t) sanitize(t::NamedTuple) = map(sanitize, t) sanitize(x::Function) = repr(x) -sanitize(d::Dict) = Dict([sanitize(k)=>sanitize(d[k]) for k in keys(d)]) +sanitize(d::Dict) = Dict(sanitize(k)=>sanitize(d[k]) for k in keys(d)) sanitize(a::Array) = sanitize.(a) +sanitize(s::Set) = Set(sanitize.(s)) sanitize(x) = x StructTypes.StructType(::Type{<:Dagger.Processor}) = StructTypes.CustomStruct() diff --git a/lib/DaggerWebDash/src/d3.jl b/lib/DaggerWebDash/src/d3.jl index fb8fc6094..bc77991cd 100644 --- a/lib/DaggerWebDash/src/d3.jl +++ b/lib/DaggerWebDash/src/d3.jl @@ -9,16 +9,22 @@ end StructTypes.StructType(::Type{LinePlot}) = StructTypes.CustomStruct() StructTypes.lower(lp::LinePlot) = "linePlot(svg_container, \"$(lp.core_key)\", \"$(lp.data_key)\", \"$(lp.xlabel)\", \"$(lp.ylabel)\")" +const GanttPlotFieldSpec = Tuple{Union{Symbol,Nothing},Symbol,Float64,Bool} struct GanttPlot core_key::Symbol id_key::Symbol - timeline_key::Symbol esat_key::Symbol psat_key::Symbol title::String + x_label::String + x_key::Union{Symbol,Nothing} + to_color::Vector{GanttPlotFieldSpec} end +GanttPlot(core_key, id_key, esat_key, psat_key; title="Overview", x_label="Time", x_key=nothing, to_color=GanttPlotFieldSpec[]) = + GanttPlot(core_key, id_key, esat_key, psat_key, + title, x_label, x_key, to_color) StructTypes.StructType(::Type{GanttPlot}) = StructTypes.CustomStruct() -StructTypes.lower(lp::GanttPlot) = "ganttPlot(svg_container, \"$(lp.core_key)\", \"$(lp.id_key)\", \"$(lp.timeline_key)\", \"$(lp.esat_key)\", \"$(lp.psat_key)\", \"$(lp.title)\")" +StructTypes.lower(lp::GanttPlot) = "ganttPlot(svg_container, \"$(lp.core_key)\", \"$(lp.id_key)\", \"$(lp.esat_key)\", \"$(lp.psat_key)\", \"$(lp.title)\", \"$(lp.x_label)\", \"$(lp.x_key)\", $(JSON3.write(sanitize(lp.to_color))))" struct GraphPlot core_key::Symbol diff --git a/lib/DaggerWebDash/src/index.html b/lib/DaggerWebDash/src/index.html index 2aa619df3..db56810d2 100644 --- a/lib/DaggerWebDash/src/index.html +++ b/lib/DaggerWebDash/src/index.html @@ -111,7 +111,10 @@ ); }; }; -function ganttPlot(container, core_key, id_key, timeline_key, esat_key, psat_key, title) { +function ganttPlot(container, core_key, id_key, esat_key, psat_key, + title, x_label, + x_key, + to_color) { var dsvg = container.append("svg") .attr("style", "border: 1px solid black"); dsvg.attr("width", screen.width-50).attr("height", 600); @@ -122,8 +125,9 @@ var xScale = d3.scaleLinear().range([0, width]); var yScale = d3.scaleOrdinal(); + var yScaleInverted = d3.scaleLinear(); var g = dsvg.append("g") - .attr("transform", "translate(" + (width_margin/2) + "," + (height_margin/2) + ")"); + .attr("transform", "translate(" + (width_margin-50) + "," + (height_margin-50) + ")"); var xg = g.append("g"); var yg = g.append("g"); var fo = g.append("foreignObject") @@ -156,6 +160,18 @@ ctx.stroke(); }); */ + var tooltip = container.append("div") + .attr("class", "tooltip") + .style("visibility", "hidden") + .style("position", "absolute") + .style("z-index", 1) + .text("My tooltip"); + canvas.on("mouseover", function(evt) { + tooltip.style("visibility", "visible"); + }); + canvas.on("mouseout", function(evt) { + tooltip.style("visibility", "hidden"); + }); // Draw title dsvg.append("text") @@ -172,44 +188,74 @@ .attr("text-anchor", "end") .attr("font-size", "16px") .attr("fill", "blue") - .text("Time"); + .text(x_label); return function(values) { var core = values[core_key], ids = values[id_key], - timelines = values[timeline_key], esat = values[esat_key], psat = values[psat_key]; // Set axis domains - //var time_min = d3.min(core, function(d){return d.timestamp;}); - //var time_max = d3.max(core, function(d){return d.timestamp;}); var time_min = seek_ts_start; var time_max = seek_ts_stop; xScale.domain([time_min, time_max]); - var allkeys = new Set(); + var key_is_event = new Map(); + + // Collect esat keys var ekeys = new Set(); for (i = 0; i < core.length; i++) { for (key in esat[i]) { ekeys.add(key); - allkeys.add(key); + key_is_event.set(key, true); } } + var ekeys_array = new Array(); + for (value of ekeys.values()) { + ekeys_array.push(value); + } + ekeys_array = ekeys_array.sort().reverse(); + + // Collect psat keys var pkeys = new Set(); - for (i = 0; i < core.length; i++) { - for (key in psat[i]) { - pkeys.add(key); - allkeys.add(key); + if (psat !== undefined) { + for (i = 0; i < core.length; i++) { + // FIXME: for (key of Object.keys(psat[i])) { + for (key of psat[i]) { + key = key[0].toString() + pkeys.add(key); + key_is_event.set(key, false); + } } } + var pkeys_array = new Array(); + for (value of pkeys.values()) { + pkeys_array.push(value); + } + pkeys_array = pkeys_array.sort(); + var allkeys_array = new Array(); - for (value of allkeys.values()) { - allkeys_array.push(value); + for (key of pkeys_array) { + allkeys_array.push(key); + } + for (key of ekeys_array) { + allkeys_array.push(key); } - allkeys_array = allkeys_array.sort(); - var stepsize = height / allkeys.size; + + var stepsize = height / allkeys_array.length; yScale.domain(allkeys_array) .range(d3.range(height, 0, -stepsize)); + yScaleInverted.domain([height, 0]) + .range([0, allkeys_array.length-1]); + + // Register tooltip handler + canvas.on("mousemove", function(evt) { + var key_idx = yScaleInverted(event.layerY); + var key = allkeys_array[Math.ceil(key_idx)]; + tooltip.style("top", event.pageY+"px") + .style("left", event.pageX+20+"px") + .text(key); + }); // Draw x-axis xg.attr("transform", "translate(0," + height + ")") @@ -236,35 +282,37 @@ var id_map = new Map(); var y = yScale(key)-stepsize; var y_bot = y+stepsize; + var is_event = key_is_event.get(key); + + // Configure color and opacity + var color = "lightgrey"; + var opacity = 0.4; + var draw_line = true; + for (c = 0; c < to_color.length; c++) { + if ((to_color[c][0] == key) || (!is_event && (to_color[c][0] === null))) { + color = to_color[c][1]; + opacity = to_color[c][2]; + draw_line = to_color[c][3]; + } + } for (i = 0; i < core.length; i++) { - // Skip irrelevant events var category = core[i].category; - var color = "lightgrey"; - var opacity = 0.4; - if (ekeys.has(key)) { - if (category != key) {continue;} - } else { - if (category == "compute") { - color = "green"; - opacity = 0.1; - } else if (category != "move") { - color = "red"; - opacity = 0.1; - } else { - continue; - } - } + var id = ids[i].toString(); + + // Skip irrelevant events + if ((is_event == true) && (key != category)) {continue;} + if ((is_event == false) && (key != id)) {continue;} // Event matched, create the block - var id = ids[i]; if (core[i].kind == "start") { id_map.set(id, i); } else { - var start_ts = time_min; if (id_map.has(id)) { var start_i = id_map.get(id); - start_ts = core[start_i].timestamp; + var start_ts = core[start_i].timestamp; id_map.delete(id); + } else { + var start_ts = time_min; } var finish_ts = core[i].timestamp; blocks.push({ @@ -281,16 +329,7 @@ for (id in id_map.keys()) { var i = id_map.get(id); var x = xScale(core[i].timestamp); - var color = "lightgrey"; var category = core[i].category; - var opacity = 0.4; - if (category == "compute") { - color = "green"; - opacity = 0.1; - } else if (category == "move") { - color = "red"; - opacity = 0.1; - } blocks.push({ x: x, y: y, @@ -300,16 +339,25 @@ }); } - // Calculate max esat - var subsat = d3.map(ekeys.has(key) ? esat : psat, function(d) { - var value = d[key]; - return value == undefined ? 0 : value; + // Calculate value min and max + var subsat = d3.map(is_event ? esat : psat, function(d) { + var value = 0; + if (is_event) { + value = d[key]; + } else { + for (pkey of d) { + if (pkey[0].toString() == key) { + value = pkey[1]; + } + } + } + return value === undefined ? 0 : value; }); var sat_min = d3.min(subsat); var sat_max = d3.max(subsat); var satScale = d3.scaleLinear() - .domain([sat_min, sat_max]) - .range([y_bot-5, y+5]); + .domain([sat_min, sat_max]) + .range([y_bot-5, y+5]); for (i = 0; i < blocks.length; i++) { var block = blocks[i]; @@ -326,49 +374,26 @@ block.y, block.width, stepsize); - - // Draw block - /* TODO: Remove me - g.append("rect") - .attr("fill", block.color) - .attr("stroke", "black") - .attr("opacity", block.opacity) - .attr("x", block.x) - .attr("y", block.y) - .attr("width", block.width) - .attr("height", stepsize); - */ } - // Draw saturation line over block - ctx.beginPath(); - ctx.globalAlpha = 1.0; - ctx.strokeStyle = "steelblue"; - var last_y = satScale(subsat[0]); - ctx.moveTo(0, last_y); - for (i = 0; i < core.length; i++) { - var sat_x = xScale(core[i].timestamp), - sat_y = satScale(subsat[i]); - ctx.lineTo(sat_x, - last_y); - ctx.lineTo(sat_x, - sat_y); - last_y = sat_y; + if (draw_line) { + // Draw saturation line over block + ctx.beginPath(); + ctx.globalAlpha = 1.0; + ctx.strokeStyle = "steelblue"; + var last_y = satScale(subsat[0]); + ctx.moveTo(0, last_y); + for (i = 0; i < core.length; i++) { + var sat_x = xScale(core[i].timestamp), + sat_y = satScale(subsat[i]); + ctx.lineTo(sat_x, + last_y); + ctx.lineTo(sat_x, + sat_y); + last_y = sat_y; + } + ctx.stroke(); } - ctx.stroke(); - - // Draw saturation line over blocks - /* TODO: Remove me - g.append("path") - .datum(d3.zip(core, subsat)) - .attr("fill", "none") - .attr("stroke", "steelblue") - .attr("stroke-width", 1.0) - .attr("d", d3.line() - .x(function(d) { return xScale(d[0].timestamp); }) - .y(function(d) { return satScale(d[1]); }) - ); - */ } }; }; @@ -753,7 +778,7 @@ // TODO: Delete unused ctxs for (i = 0; i < payload.ctxs.length; i++) { ctx = payload.ctxs[i]; - ctxfn = eval(ctx) + ctxfn = eval(ctx); ctxs.push(ctxfn); } setTimeout(function(){drawCtxs()}, 1000); diff --git a/lib/DaggerWebDash/test/runtests.jl b/lib/DaggerWebDash/test/runtests.jl index b395020dc..8c0b9818d 100644 --- a/lib/DaggerWebDash/test/runtests.jl +++ b/lib/DaggerWebDash/test/runtests.jl @@ -9,7 +9,7 @@ using Test ml[:core] = TimespanLogging.Events.CoreMetrics() ml[:id] = TimespanLogging.Events.IDMetrics() #profile && (ml[:profile] = DaggerWebDash.ProfileMetrics()) - ml[:wsat] = TimespanLogging.Events.WorkerSaturation() + ml[:wsat] = Dagger.Events.WorkerSaturation() ml[:loadavg] = TimespanLogging.Events.CPULoadAverages() ml[:bytes] = Dagger.Events.BytesAllocd() ml[:mem] = TimespanLogging.Events.MemoryFree() @@ -22,7 +22,7 @@ using Test d3r = DaggerWebDash.D3Renderer(8080) #; seek_store=ts) push!(lw.creation_handlers, d3r) push!(lw.deletion_handlers, d3r) - push!(d3r, GanttPlot(:core, :id, :timeline, :esat, :psat, "Overview")) + push!(d3r, GanttPlot(:core, :id, :esat, :psat)) # TODO: push!(d3r, ProfileViewer(:core, :profile, "Profile Viewer")) push!(d3r, LinePlot(:core, :wsat, "Worker Saturation", "Running Tasks")) push!(d3r, LinePlot(:core, :loadavg, "CPU Load Average", "Average Running Threads")) diff --git a/lib/TimespanLogging/src/extras.jl b/lib/TimespanLogging/src/extras.jl index f2e28dddd..5720307b4 100644 --- a/lib/TimespanLogging/src/extras.jl +++ b/lib/TimespanLogging/src/extras.jl @@ -82,30 +82,6 @@ function (es::EventSaturation)(ev::Event{:finish}) NamedTuple(filter(x->x[2]>0, es.saturation)) end -""" - WorkerSaturation - -Tracks the compute saturation (running tasks). -""" -mutable struct WorkerSaturation - saturation::Int -end -WorkerSaturation() = WorkerSaturation(0) -init_similar(::WorkerSaturation) = WorkerSaturation() - -function (ws::WorkerSaturation)(ev::Event{:start}) - if ev.category == :compute - ws.saturation += 1 - end - ws.saturation -end -function (ws::WorkerSaturation)(ev::Event{:finish}) - if ev.category == :compute - ws.saturation -= 1 - end - ws.saturation -end - "Debugging metric, used to log event start/finish via `@debug`." struct DebugMetrics sat::EventSaturation diff --git a/src/lib/logging-events.jl b/src/lib/logging-events.jl index d5145d073..83a0ff137 100644 --- a/src/lib/logging-events.jl +++ b/src/lib/logging-events.jl @@ -52,15 +52,45 @@ function (ps::ProcessorSaturation)(ev::Event{:start}) old = get(ps.saturation, proc, 0) ps.saturation[proc] = old + 1 end - filter(x->x[2]>0, ps.saturation) + # FIXME: JSON doesn't support complex arguments as object keys, so use a vector of tuples instead + #filter(x->x[2]>0, ps.saturation) + return map(x->(x[1], x[2]), filter(x->x[2]>0, collect(ps.saturation))) end function (ps::ProcessorSaturation)(ev::Event{:finish}) if ev.category == :compute proc = ev.timeline.to_proc old = get(ps.saturation, proc, 0) ps.saturation[proc] = old - 1 + if old == 1 + delete!(ps.saturation, proc) + end + end + #filter(x->x[2]>0, ps.saturation) + return map(x->(x[1], x[2]), filter(x->x[2]>0, collect(ps.saturation))) +end + +""" + WorkerSaturation + +Tracks the compute saturation (running tasks). +""" +mutable struct WorkerSaturation + saturation::Int +end +WorkerSaturation() = WorkerSaturation(0) +init_similar(::WorkerSaturation) = WorkerSaturation() + +function (ws::WorkerSaturation)(ev::Event{:start}) + if ev.category == :compute + ws.saturation += 1 + end + ws.saturation +end +function (ws::WorkerSaturation)(ev::Event{:finish}) + if ev.category == :compute + ws.saturation -= 1 end - filter(x->x[2]>0, ps.saturation) + ws.saturation end end # module Events diff --git a/test/logging.jl b/test/logging.jl index 7eafcedc2..c4431bba5 100644 --- a/test/logging.jl +++ b/test/logging.jl @@ -65,7 +65,7 @@ import TimespanLogging: Timespan, Event, Events, LocalEventLog, MultiEventLog ml[:core] = Events.CoreMetrics() ml[:id] = Events.IDMetrics() ml[:timeline] = Events.TimelineMetrics() - ml[:wsat] = Events.WorkerSaturation() + ml[:wsat] = Dagger.Events.WorkerSaturation() ml[:loadavg] = Events.CPULoadAverages() ml[:bytes] = Dagger.Events.BytesAllocd() ml[:mem] = Events.MemoryFree()