// // TimeLineView.cs // // Author: // Lluis Sanchez Gual // // Copyright (c) 2010 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using Gtk; using Gdk; using System.Collections.Generic; using MonoDevelop.Core.Instrumentation; using MonoDevelop.Components; using System.Linq; using Mono.TextEditor; namespace Mono.Instrumentation.Monitor { [System.ComponentModel.ToolboxItem(true)] class TimeLineView: EventBox { Counter mainCounter; CounterValue mainValue; List extraCounters = new List (); List data; List hostSpots = new List (); int lastHeight; int lastWidth; int baseTime; CounterValueInfo focusedValue; CounterValueInfo overValue; Pango.Layout layout; double scale = 300; // Pixels per second const int padding = 3; const int LineSpacing = 1; const int MarkerWidth = 20; const int LineEndWidth = 20; const int ExpanderSize = 8; const int ChildIndent = 20; const int SelectedValuePadding = 3; class HotSpot { public Rectangle Rect; public System.Action Action; public System.Action OnMouseOver; public System.Action OnMouseLeave; public bool IsOver; } class CounterValueInfo { public DateTime Time; public Counter Counter; public string Trace; public IEnumerable TimerTraces; public List ExpandedTimerTraces; public bool Expanded; public bool CanExpand; public TimeSpan Duration; } public TimeLineView () { layout = new Pango.Layout (this.PangoContext); layout.FontDescription = Pango.FontDescription.FromString ("Tahoma 8"); Events |= EventMask.PointerMotionMask | EventMask.LeaveNotifyMask; } public void Clear () { mainCounter = null; extraCounters.Clear (); data = null; } public void SetMainCounter (Counter c, CounterValue value) { mainCounter = c; mainValue = value; data = null; Scale = scale; QueueDraw (); } public void AddCounter (ChartSerieInfo c) { extraCounters.Add (c); data = null; QueueDraw (); } public void ExpandAll () { } public void CollapseAll () { } bool singleThread; public bool SingleThread { get { return singleThread; } set { singleThread = value; BuildData (); QueueDraw (); } } double timeScale; public double TimeScale { get { return timeScale; } set { timeScale = value; Scale = (value * 300) / 100; } } int zoom; public int Zoom { get { return zoom; } set { zoom = value; QueueDraw (); } } public double Scale { get { return this.scale; } set { this.scale = value; QueueDraw (); } } void BuildData () { data = new List (); if (mainCounter == null) return; DateTime lastTime = mainValue.TimeStamp + mainValue.Duration; FillTimerTraces (mainValue.GetTimerTraces (), data, mainValue.TimeStamp, lastTime); FixValueList (data, lastTime); foreach (CounterValueInfo val in data) { ExpandValue (val); // val.Expanded = true; } } void FillTimerTraces (IEnumerable traces, List list, DateTime startTime, DateTime endTime) { if (!traces.Any ()) { GetValues (list, startTime, endTime, false, false); } else { GetValues (list, startTime, traces.First ().Timestamp, false, false); } foreach (TimerTrace tt in traces) { CounterValueInfo v = new CounterValueInfo (); v.Time = tt.Timestamp; v.Trace = tt.Message; v.Counter = mainCounter; v.CanExpand = true; list.Add (v); } } void ExpandValue (CounterValueInfo val) { List list = new List (); if (val.TimerTraces != null) { FillTimerTraces (val.TimerTraces, list, val.Time, val.Time + val.Duration); } else { GetValues (list, val.Time, val.Time + val.Duration, false, false); } if (list.Count == 0) val.CanExpand = false; FixValueList (list, val.Time + val.Duration); val.ExpandedTimerTraces = list; } void GetValues (List list, DateTime startTime, DateTime endTime, bool includeStart, bool includeEnd) { foreach (Counter c in App.Service.GetCounters ()) { foreach (CounterValue cval in c.GetValuesBetween (startTime, endTime)) { if (singleThread && cval.ThreadId != mainValue.ThreadId) continue; if (!includeStart && cval.TimeStamp == startTime) continue; if (!includeEnd && cval.TimeStamp == endTime) continue; CounterValueInfo v = new CounterValueInfo (); v.Time = cval.TimeStamp; v.Counter = c; v.Trace = "[" + c.Name + ": " + cval.Value + "]"; if (!string.IsNullOrEmpty (cval.Message)) v.Trace = cval.Message + " " + v.Trace; if (cval.HasTimerTraces) { v.TimerTraces = cval.GetTimerTraces (); v.Duration = cval.Duration; v.CanExpand = true; } list.Add (v); } } } void ToggleExpand (CounterValueInfo val) { val.Expanded = !val.Expanded; if (!val.Expanded || val.ExpandedTimerTraces != null) { QueueDraw (); return; } ExpandValue (val); QueueDraw (); } void FixValueList (List list, DateTime endTime) { if (list.Count == 0) return; list.Sort (delegate (CounterValueInfo v1, CounterValueInfo v2) { return v1.Time.CompareTo (v2.Time); }); for (int n=0; n n + 1) || (val.TimerTraces.Any ()); } else val.Duration = list [n + 1].Time - val.Time; } list [list.Count - 1].Duration = endTime - list [list.Count - 1].Time; } HotSpot AddHotSpot (double x, double y, double w, double h) { HotSpot hp = new HotSpot (); hp.Rect = new Rectangle ((int)x, (int)y, (int)w, (int)h); hostSpots.Add (hp); return hp; } uint timeAnim = 0; int destBaseTime; void SetBaseTime (int dy) { if (timeAnim != 0) GLib.Source.Remove (timeAnim); destBaseTime = dy; timeAnim = GLib.Timeout.Add (40, delegate { baseTime = baseTime + (destBaseTime - baseTime) / 2; QueueDraw (); return baseTime != destBaseTime; }); } protected override bool OnMotionNotifyEvent (EventMotion evnt) { foreach (HotSpot hp in hostSpots) { if (hp.Rect.Contains ((int)evnt.X, (int)evnt.Y) && hp.OnMouseOver != null) { if (!hp.IsOver && hp.OnMouseOver != null) { hp.IsOver = true; hp.OnMouseOver (); } } else if (hp.IsOver) { if (hp.IsOver && hp.OnMouseLeave != null) { hp.IsOver = false; hp.OnMouseLeave (); } } } return base.OnMotionNotifyEvent (evnt); } protected override bool OnLeaveNotifyEvent (EventCrossing evnt) { foreach (HotSpot hp in hostSpots) { if (hp.IsOver && hp.OnMouseLeave != null) { hp.IsOver = false; hp.OnMouseLeave (); break; } } return base.OnLeaveNotifyEvent (evnt); } protected override bool OnButtonPressEvent (EventButton evnt) { foreach (HotSpot hp in hostSpots) { if (hp.Rect.Contains ((int)evnt.X, (int)evnt.Y) && hp.Action != null) { hp.Action (); break; } } return base.OnButtonPressEvent (evnt); } protected override bool OnExposeEvent (EventExpose evnt) { if (data == null) BuildData (); hostSpots.Clear (); int ytop = padding; int markerX = 3; int lx = markerX + MarkerWidth + 1; int tx = 250; int ty = ytop; int maxx = lx; int maxy = 0; DateTime initialTime = mainValue.TimeStamp; Cairo.Context ctx = CairoHelper.Create (GdkWindow); Gdk.GC gc = new Gdk.GC (GdkWindow); gc.RgbFgColor = Style.White; GdkWindow.DrawRectangle (gc, true, 0, 0, Allocation.Width, Allocation.Height); // Draw full time marker ctx.NewPath (); ctx.Rectangle (markerX, ytop + baseTime + 0.5, MarkerWidth/2, ((mainValue.Duration.TotalMilliseconds * scale) / 1000)); HslColor hsl = Style.Foreground (Gtk.StateType.Normal); hsl.L = 0.8; ctx.SetSourceColor (hsl); ctx.Fill (); // Draw values foreach (CounterValueInfo val in data) { DrawValue (ctx, gc, initialTime, ytop, lx, tx, ref ty, ref maxx, ref maxy, 0, val); } if (ty > maxy) maxy = ty; int totalms = (int) mainValue.Duration.TotalMilliseconds; int marks = (totalms / 1000) + 1; ctx.LineWidth = 1; gc.RgbFgColor = Style.Foreground (Gtk.StateType.Normal); for (int n=0; n<=marks; n++) { ctx.NewPath (); int y = ytop + (int) (n * scale) + baseTime; ctx.MoveTo (markerX, y + 0.5); ctx.LineTo (markerX + MarkerWidth, y + 0.5); ctx.SetSourceColor (Style.Foreground (Gtk.StateType.Normal).ToCairoColor ()); ctx.Stroke (); y += 2; layout.SetText (n + "s"); GdkWindow.DrawLayout (gc, markerX + 1, y + 2, layout); int tw, th; layout.GetPixelSize (out tw, out th); y += th; if (y > maxy) maxy = y; } ((IDisposable)ctx).Dispose (); maxy += padding; maxx += padding; if (lastHeight != maxy || lastWidth != maxx) { lastWidth = maxx; lastHeight = maxy; SetSizeRequest (maxx, maxy); } return true; } void DrawValue (Cairo.Context ctx, Gdk.GC gc, DateTime initialTime, int ytop, int lx, int tx, ref int ty, ref int maxx, ref int maxy, int indent, CounterValueInfo val) { Gdk.Color color; if (val.Counter != null) color = val.Counter.GetColor (); else color = Style.Black; // Draw text gc.RgbFgColor = color; double ms = (val.Time - initialTime).TotalMilliseconds; string txt = (ms / 1000).ToString ("0.00000") + ": " + (val.Duration.TotalMilliseconds / 1000).ToString ("0.00000") + " " + val.Trace; layout.SetText (txt); GdkWindow.DrawLayout (gc, tx + indent, ty, layout); int tw, th; layout.GetPixelSize (out tw, out th); if (tx + tw + indent > maxx) maxx = tx + tw + indent; HotSpot hp = AddHotSpot (tx + indent, ty, tw, th); int tempTy = ty; hp.Action = delegate { int ytm = ytop + (int) ((ms * scale) / 1000); SetBaseTime ((int) (tempTy + (th / 2) + 0.5) - ytm); }; hp.OnMouseOver += delegate { overValue = val; QueueDraw (); }; hp.Action += delegate { focusedValue = val; QueueDraw (); }; // Draw time marker int ytime = ytop + (int) ((ms * scale) / 1000) + baseTime; if (val == focusedValue || val == overValue) { ctx.NewPath (); double dx = val == focusedValue ? 0 : 2; ctx.Rectangle (lx + 0.5 + dx - SelectedValuePadding, ytime + 0.5, LineEndWidth - dx*2 + SelectedValuePadding, ((val.Duration.TotalMilliseconds * scale) / 1000)); HslColor hsl = color; hsl.L = val == focusedValue ? 0.9 : 0.8; ctx.SetSourceColor (hsl); ctx.Fill (); } ctx.NewPath (); ctx.LineWidth = 1; ctx.MoveTo (lx + 0.5, ytime + 0.5); ctx.LineTo (lx + LineEndWidth + 0.5, ytime + 0.5); ctx.LineTo (tx - 3 - LineEndWidth + 0.5, ty + (th / 2) + 0.5); ctx.LineTo (tx + indent - 3 + 0.5, ty + (th / 2) + 0.5); ctx.SetSourceColor (color.ToCairoColor ()); ctx.Stroke (); // Expander bool incLine = true; if (val.CanExpand) { double ex = tx + indent - 3 - ExpanderSize - 2 + 0.5; double ey = ty + (th / 2) - (ExpanderSize/2) + 0.5; hp = AddHotSpot (ex, ey, ExpanderSize, ExpanderSize); DrawExpander (ctx, ex, ey, val.Expanded, false); hp.OnMouseOver = delegate { using (Cairo.Context c = CairoHelper.Create (GdkWindow)) { DrawExpander (c, ex, ey, val.Expanded, true); } }; hp.OnMouseLeave = delegate { using (Cairo.Context c = CairoHelper.Create (GdkWindow)) { DrawExpander (c, ex, ey, val.Expanded, false); } }; hp.Action = delegate { ToggleExpand (val); }; if (val.Expanded && val.ExpandedTimerTraces.Count > 0) { ty += th + LineSpacing; foreach (CounterValueInfo cv in val.ExpandedTimerTraces) DrawValue (ctx, gc, initialTime, ytop, lx, tx, ref ty, ref maxx, ref maxy, indent + ChildIndent, cv); incLine = false; } } if (incLine) ty += th + LineSpacing; if (ytime > maxy) maxy = ytime; } void DrawExpander (Cairo.Context ctx, double ex, double ey, bool expanded, bool hilight) { ctx.NewPath (); ctx.LineWidth = 1; ctx.Rectangle (ex, ey, ExpanderSize, ExpanderSize); if (hilight) ctx.SetSourceColor (Style.Background (Gtk.StateType.Normal).ToCairoColor ()); else ctx.SetSourceColor (Style.White.ToCairoColor ()); ctx.FillPreserve (); ctx.SetSourceColor (Style.Foreground (Gtk.StateType.Normal).ToCairoColor ()); ctx.Stroke (); ctx.NewPath (); ctx.MoveTo (ex + 2, ey + (ExpanderSize/2)); ctx.RelLineTo (ExpanderSize - 4, 0); if (!expanded) { ctx.MoveTo (ex + (ExpanderSize/2), ey + 2); ctx.RelLineTo (0, ExpanderSize - 4); } ctx.Stroke (); } } }