Permalink
Browse files

Sparklines: move to SVGs

This means far fewer allocations on chart generation, better compression
for most, better scalability, eliminates the HighDPI cache split, and
almost eliminates the System.Web.UI.DataVisualization.Charting library
(one use in HAProxy.Traffic left).
This is just a first pass, more to optimize.
  • Loading branch information...
1 parent c8451dc commit 55a2e3aea1b877b5ff19f4d77a5bd92c94bdf12f @NickCraver NickCraver committed Dec 30, 2015
@@ -2,7 +2,7 @@
{
public interface IGraphPoint
{
- long DateEpoch { get; set; }
+ long DateEpoch { get; }
}
public class GraphPoint : IGraphPoint
@@ -115,7 +115,7 @@ public override Task<List<DoubleGraphPoint>> GetUtilizationAsync(Interface @inte
return Task.FromResult(FilterHistory<Interface.InterfaceUtilization, DoubleGraphPoint>(node?.GetInterfaceUtilizationHistory(@interface), start, end).ToList());
}
- private static IEnumerable<TResult> FilterHistory<T, TResult>(List<T> list, DateTime? start, DateTime? end) where T : IGraphPoint, TResult, new()
+ private static IEnumerable<TResult> FilterHistory<T, TResult>(List<T> list, DateTime? start, DateTime? end) where T : GraphPoint, TResult, new()
{
if (list == null)
return Enumerable.Empty<TResult>();
@@ -1,25 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using StackExchange.Opserver.Data.Dashboard;
namespace StackExchange.Opserver.Data.SQL
{
public partial class SQLInstance
{
- private Cache<List<SQLCPUEvent>> _cpuHistoryLastHour;
- public Cache<List<SQLCPUEvent>> CPUHistoryLastHour
+ private Cache<List<ResourceEvent>> _cpuHistoryLastHour;
+ public Cache<List<ResourceEvent>> ResourceHistory
{
get
{
- return _cpuHistoryLastHour ?? (_cpuHistoryLastHour = new Cache<List<SQLCPUEvent>>
+ return _cpuHistoryLastHour ?? (_cpuHistoryLastHour = new Cache<List<ResourceEvent>>
{
CacheForSeconds = RefreshInterval,
- UpdateCache = UpdateFromSql("CPUHistoryLastHour", async conn =>
+ UpdateCache = UpdateFromSql(nameof(ResourceHistory), async conn =>
{
- var sql = GetFetchSQL<SQLCPUEvent>();
- var result = (await conn.QueryAsync<SQLCPUEvent>(sql, new {maxEvents = 60}))
- .OrderBy(e => e.EventTime)
- .ToList();
+ var sql = GetFetchSQL<ResourceEvent>();
+ var result = (await conn.QueryAsync<ResourceEvent>(sql)).AsList();
CurrentCPUPercent = result.Count > 0 ? result.Last().ProcessUtilization : (int?) null;
return result;
})
@@ -29,27 +28,30 @@ public Cache<List<SQLCPUEvent>> CPUHistoryLastHour
public int? CurrentCPUPercent { get; set; }
- public class SQLCPUEvent : ISQLVersioned
+ public class ResourceEvent : ISQLVersioned, IGraphPoint
{
public Version MinVersion => SQLServerVersions.SQL2005.RTM;
+ private long? _dateEpoch;
+ public long DateEpoch => _dateEpoch ?? (_dateEpoch = EventTime.ToEpochTime()).Value;
public DateTime EventTime { get; internal set; }
public int ProcessUtilization { get; internal set; }
+ public int MemoryUtilization { get; internal set; }
public int SystemIdle { get; internal set; }
public int ExternalProcessUtilization => 100 - SystemIdle - ProcessUtilization;
public string GetFetchSQL(Version v) => @"
-Select Top (@maxEvents)
- DateAdd(s, (timestamp - (osi.cpu_ticks / Convert(Float, (osi.cpu_ticks / osi.ms_ticks)))) / 1000, GETDATE()) AS EventTime,
+Select DateAdd(s, (timestamp - (osi.cpu_ticks / Convert(Float, (osi.cpu_ticks / osi.ms_ticks)))) / 1000, GETDATE()) AS EventTime,
Record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') as SystemIdle,
- Record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') as ProcessUtilization
+ Record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') as ProcessUtilization,
+ Record.value('(./Record/SchedulerMonitorEvent/SystemHealth/MemoryUtilization)[1]', 'int') as MemoryUtilization
From (Select timestamp,
convert(xml, record) As Record
From sys.dm_os_ring_buffers
Where ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR'
And record Like '%<SystemHealth>%') x
Cross Join sys.dm_os_sys_info osi
-Order By timestamp Desc";
+Order By timestamp";
}
}
}
@@ -52,7 +52,7 @@ public override IEnumerable<Cache> DataPollers
yield return ServerProperties;
yield return Configuration;
yield return Databases;
- yield return CPUHistoryLastHour;
+ yield return ResourceHistory;
yield return JobSummary;
yield return PerfCounters;
yield return MemoryClerkSummary;
View
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
-VisualStudioVersion = 14.0.22823.1
+VisualStudioVersion = 14.0.24720.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Opserver.Core", "Opserver.Core\Opserver.Core.csproj", "{C58AFF99-F4D9-4A83-866E-18DA0A633F6B}"
EndProject
@@ -1,8 +1,4 @@
-if (window.devicePixelRatio >= 2) {
- Cookies.set('highDPI', 'true', { expires: 365 * 10, path: '/' });
-}
-
-window.Status = (function() {
+window.Status = (function() {
var ajaxLoaders = {},
registeredRefreshes = {};
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Drawing;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
-using System.Web.UI.DataVisualization.Charting;
using StackExchange.Opserver.Data.Dashboard;
using StackExchange.Opserver.Data.SQL;
using StackExchange.Opserver.Helpers;
@@ -14,156 +13,94 @@ namespace StackExchange.Opserver.Controllers
{
public partial class GraphController
{
- private static DateTime SparkStart => DateTime.UtcNow.AddHours(-24);
- private static int SparkPoints => Current.ViewSettings.SparklineChartWidth * 2;
+ private const int SparkHours = 24;
+ private static DateTime SparkStart => DateTime.UtcNow.AddHours(-SparkHours);
+ private const int SparkPoints = 500;
- [OutputCache(Duration = 120, VaryByParam = "id", VaryByContentEncoding = "gzip;deflate", VaryByCustom="highDPI")]
+ [OutputCache(Duration = 120, VaryByParam = "id", VaryByContentEncoding = "gzip;deflate")]
[Route("graph/cpu/spark"), AlsoAllow(Roles.InternalRequest)]
- public async Task<ActionResult> CPUSpark(string id)
+ public async Task<ActionResult> CPUSparkSvg(string id)
{
var node = DashboardData.GetNodeById(id);
if (node == null) return ContentNotFound();
- var chart = GetSparkChart(max: 100);
- var dataPoints = await node.GetCPUUtilization(SparkStart, null, SparkPoints);
- AddPoints(chart, dataPoints, p => p.Value.GetValueOrDefault(0));
-
- return chart.ToResult();
+ var points = await node.GetCPUUtilization(SparkStart, null, SparkPoints);
+ return SparkSVG(points, 100, p => p.Value.GetValueOrDefault());
}
- [OutputCache(Duration = 120, VaryByParam = "id", VaryByContentEncoding = "gzip;deflate", VaryByCustom = "highDPI")]
+ [OutputCache(Duration = 120, VaryByParam = "id", VaryByContentEncoding = "gzip;deflate")]
[Route("graph/memory/spark"), AlsoAllow(Roles.InternalRequest)]
public async Task<ActionResult> MemorySpark(string id)
{
var node = DashboardData.GetNodeById(id);
if (node?.TotalMemory == null) return ContentNotFound($"Could not determine total memory for '{id}'");
-
- var chart = GetSparkChart(max: node.TotalMemory);
- var dataPoints = await node.GetMemoryUtilization(SparkStart, null, SparkPoints);
- AddPoints(chart, dataPoints, p => p.Value.GetValueOrDefault(0));
-
- return chart.ToResult();
+ var points = await node.GetMemoryUtilization(SparkStart, null, SparkPoints);
+ return SparkSVG(points, Convert.ToInt64(node.TotalMemory.GetValueOrDefault()), p => p.Value.GetValueOrDefault());
}
- [OutputCache(Duration = 120, VaryByParam = "id", VaryByContentEncoding = "gzip;deflate", VaryByCustom = "highDPI")]
+ [OutputCache(Duration = 120, VaryByParam = "id", VaryByContentEncoding = "gzip;deflate")]
[Route("graph/network/spark"), AlsoAllow(Roles.InternalRequest)]
public async Task<ActionResult> NetworkSpark(string id)
{
var node = DashboardData.GetNodeById(id);
if (node == null) return ContentNotFound();
- var chart = GetSparkChart();
- var dataPoints = await node.GetNetworkUtilization(SparkStart, null, SparkPoints);
- AddPoints(chart, dataPoints, p => (p.Value + p.BottomValue).GetValueOrDefault(0));
-
- return chart.ToResult();
+ var points = await node.GetNetworkUtilization(SparkStart, null, SparkPoints);
+ return SparkSVG(points, Convert.ToInt64(points.Max(p => p.Value + p.BottomValue).GetValueOrDefault()), p => (p.Value + p.BottomValue).GetValueOrDefault());
}
- [OutputCache(Duration = 120, VaryByParam = "id;iid", VaryByContentEncoding = "gzip;deflate", VaryByCustom = "highDPI")]
- [Route("graph/interface/{direction}/spark")]
- public async Task<ActionResult> InterfaceOutSpark(string direction, string id, string iid)
+ [OutputCache(Duration = 120, VaryByParam = "id;iid", VaryByContentEncoding = "gzip;deflate")]
+ [Route("graph/interface/{direction}/spark"), AlsoAllow(Roles.InternalRequest)]
+ public async Task<ActionResult> InterfaceSpark(string direction, string id, string iid)
{
var iface = DashboardData.GetNodeById(id)?.GetInterface(iid);
if (iface == null) return ContentNotFound();
- var chart = GetSparkChart();
- var dataPoints = await iface.GetUtilization(SparkStart, null, SparkPoints);
+ var points = await iface.GetUtilization(SparkStart, null, SparkPoints);
Func<DoubleGraphPoint, double> getter = p => p.Value.GetValueOrDefault(0);
if (direction == "out") getter = p => p.BottomValue.GetValueOrDefault(0);
- AddPoints(chart, dataPoints, getter);
- return chart.ToResult();
+ return SparkSVG(points, Convert.ToInt64(points.Max(getter)), getter);
}
- [OutputCache(Duration = 120, VaryByParam = "node", VaryByContentEncoding = "gzip;deflate", VaryByCustom = "highDPI")]
+ [OutputCache(Duration = 120, VaryByParam = "node", VaryByContentEncoding = "gzip;deflate")]
[Route("graph/sql/cpu/spark")]
public ActionResult SQLCPUSpark(string node)
{
var instance = SQLInstance.Get(node);
if (instance == null) return ContentNotFound($"SQLNode not found with name = '{node}'");
+ var start = DateTime.UtcNow.AddHours(-1);
+ var points = instance.ResourceHistory.Data?.Where(p => p.EventTime >= start)
+ ?? Enumerable.Empty<SQLInstance.ResourceEvent>();
- var chart = GetSparkChart(height: 20, width: 100, max: 100);
- var dataPoints = instance.CPUHistoryLastHour;
-
- var area = chart.ChartAreas.First();
- area.AxisX.Minimum = DateTime.UtcNow.AddHours(-1).ToOADate();
- area.AxisX.Maximum = DateTime.UtcNow.ToOADate();
- area.AxisX.LineColor = Color.Transparent;
-
- if (dataPoints.HasData())
- {
- var series = chart.Series.First();
- foreach (var cpu in dataPoints.Data)
- {
- series.Points.Add(new DataPoint(cpu.EventTime.ToOADate(), cpu.ProcessUtilization));
- }
- }
-
- return chart.ToResult();
+ return SparkSVG(points, 100, p => p.ProcessUtilization, start);
}
- private static void AddPoints<T>(Chart chart, IEnumerable<T> points, Func<T, double> getValue) where T : IGraphPoint
+ private FileResult SparkSVG<T>(IEnumerable<T> points, long max, Func<T, double> getVal, DateTime? start = null) where T : IGraphPoint
{
- var series = chart.Series.First();
+ const string color = "#008cba";
+ const int height = 50,
+ width = SparkPoints;
+ long nowEpoch = DateTime.UtcNow.ToEpochTime(),
+ startEpoch = (start ?? SparkStart).ToEpochTime(),
+ divisor = max/50,
+ range = (nowEpoch - startEpoch)/width;
+
+ var sb = new StringBuilder().AppendFormat(@"
+<svg version=""1.1"" baseProfile=""full"" width=""{0}"" height=""{1}"" xmlns=""http://www.w3.org/2000/svg"">
+ <line x1=""0"" y1=""{1}"" x2=""{0}"" y2=""{1}"" stroke=""{2}"" stroke-width=""1"" />
+ <g fill=""{2}"" stroke=""none"">
+ <path d=""M0 50 ", width.ToString(), height.ToString(), color);
foreach (var p in points)
{
- series.Points.Add(new DataPoint(p.DateEpoch.ToOLEDate(), getValue(p)));
+ sb.Append("L")
+ .Append((p.DateEpoch - startEpoch) / range).Append(" ")
+ .Append((height - getVal(p) / divisor).ToString("n1")).Append(" ");
}
- }
-
- private Chart GetSparkChart(
- int height = Current.ViewSettings.SparklineChartHeight,
- int width = Current.ViewSettings.SparklineChartWidth,
- double? max = null)
- {
- if (Current.IsHighDPI)
- {
- height *= 2;
- width *= 2;
- }
- var chart = GetChart(height, width);
- var area = GetSparkChartArea(max);
- var series = new Series("Main")
- {
- ChartType = SeriesChartType.Area,
- XValueType = ChartValueType.DateTime,
- Color = ColorTranslator.FromHtml("#008cba"), // ColorTranslator.FromHtml("#c6d5e2"),
- EmptyPointStyle = {Color = Color.Transparent, BackSecondaryColor = Color.Transparent}
- };
- chart.Series.Add(series);
- chart.ChartAreas.Add(area);
- return chart;
- }
-
- private static ChartArea GetSparkChartArea(double? max = null)
- {
- var area = new ChartArea("area")
- {
- BackColor = Color.Transparent,
- Position = new ElementPosition(0, 0, 100, 100),
- InnerPlotPosition = new ElementPosition(0, 0, 100, 100),
- AxisY =
- {
- MaximumAutoSize = 100,
- LabelStyle = { Enabled = false },
- MajorGrid = { Enabled = false },
- MajorTickMark = { Enabled = false },
- LineColor = Color.Transparent,
- LineDashStyle = ChartDashStyle.Dot,
- },
- AxisX =
- {
- MaximumAutoSize = 100,
- LabelStyle = { Enabled = false },
- Maximum = DateTime.UtcNow.ToOADate(),
- Minimum = SparkStart.ToOADate(),
- MajorGrid = { Enabled = false },
- LineColor = ColorTranslator.FromHtml("#008cba") // ColorTranslator.FromHtml("#a3c0d7")
- }
- };
-
- if (max.HasValue)
- area.AxisY.Maximum = max.Value;
+ sb.AppendFormat(@"L{0} {1} z""/>
+ </g>
+</svg>", width.ToString(), height.ToString());
- return area;
+ var bytes = Encoding.UTF8.GetBytes(sb.ToString());
+ return new FileContentResult(bytes, "image/svg+xml");
}
}
}
View
@@ -32,20 +32,6 @@ public static partial class Current
public static bool IsAjaxRequest => Request != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest";
/// <summary>
- /// Whether to render chart images at double resolution or not
- /// </summary>
- /// <remarks>Long-term, this will need updating to observe the pixel ratio which will grow to higher than 2 as displays improve</remarks>
- public static bool IsHighDPI
- {
- get
- {
- // HACK, Hackity hack hack hack, but it works for now.
- var cookie = Request.Cookies["highDPI"];
- return cookie != null && cookie.Value == "true";
- }
- }
-
- /// <summary>
/// Gets if the current request is for a mobile view
/// </summary>
public static bool IsMobile => new HttpContextWrapper(Context).GetOverriddenBrowser().IsMobileDevice;
@@ -105,15 +105,6 @@ protected void Application_EndRequest()
MiniProfiler.Stop();
}
- public override string GetVaryByCustomString(HttpContext context, string arg)
- {
- if (arg.ToLower() == "highDPI")
- {
- return Current.IsHighDPI.ToString();
- }
- return base.GetVaryByCustomString(context, arg);
- }
-
private static void GetCustomErrorData(Exception ex, HttpContext context, Dictionary<string, string> data)
{
// everything below needs a context

0 comments on commit 55a2e3a

Please sign in to comment.