1 /*
2  * Copyright © 2008 Chris Wilson
3  *
4  * Permission to use, copy, modify, distribute, and sell this software
5  * and its documentation for any purpose is hereby granted without
6  * fee, provided that the above copyright notice appear in all copies
7  * and that both that copyright notice and this permission notice
8  * appear in supporting documentation, and that the name of the
9  * copyright holders not be used in advertising or publicity
10  * pertaining to distribution of the software without specific,
11  * written prior permission. The copyright holders make no
12  * representations about the suitability of this software for any
13  * purpose.  It is provided "as is" without express or implied
14  * warranty.
15  *
16  * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
17  * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
18  * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
19  * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
20  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
21  * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
22  * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
23  * SOFTWARE.
24  *
25  * Authors: Chris Wilson <chris@chris-wilson.co.uk>
26  */
27 
28 #include "cairo-perf.h"
29 #include "cairo-perf-graph.h"
30 
31 #include <gtk/gtk.h>
32 
33 struct _GraphView {
34     GtkWidget widget;
35 
36     test_case_t *cases;
37     cairo_perf_report_t *reports;
38     int num_reports;
39     double ymin, ymax;
40 
41     int selected_report;
42 };
43 
44 typedef struct _GraphViewClass {
45     GtkWidgetClass parent_class;
46 } GraphViewClass;
47 
48 static GType graph_view_get_type (void);
49 
50 enum {
51     REPORT_SELECTED,
52     LAST_SIGNAL
53 };
54 
55 static guint signals[LAST_SIGNAL];
56 
G_DEFINE_TYPE(GraphView,graph_view,GTK_TYPE_WIDGET)57 G_DEFINE_TYPE (GraphView, graph_view, GTK_TYPE_WIDGET)
58 
59 static void
60 draw_baseline_performance (test_case_t		*cases,
61 			   cairo_perf_report_t	*reports,
62 			   int			 num_reports,
63 			   cairo_t		*cr,
64 			   const cairo_matrix_t *m)
65 {
66     test_report_t **tests;
67     double dots[2] = { 0, 1.};
68     int i;
69 
70     tests = xmalloc (num_reports * sizeof (test_report_t *));
71     for (i = 0; i < num_reports; i++)
72 	tests[i] = reports[i].tests;
73 
74     while (cases->backend != NULL) {
75 	test_report_t *min_test;
76 	double baseline, last_y;
77 	double x, y;
78 
79 	if (! cases->shown) {
80 	    cases++;
81 	    continue;
82 	}
83 
84 	min_test = cases->min_test;
85 
86 	for (i = 0; i < num_reports; i++) {
87 	    while (tests[i]->name &&
88 		test_report_cmp_backend_then_name (tests[i], min_test) < 0)
89 	    {
90 		tests[i]++;
91 	    }
92 	}
93 
94 	/* first the stroke */
95 	cairo_save (cr);
96 	cairo_set_line_width (cr, 2.);
97 	gdk_cairo_set_source_color (cr, &cases->color);
98 	for (i = 0; i < num_reports; i++) {
99 	    if (tests[i]->name &&
100 		test_report_cmp_backend_then_name (tests[i], min_test) == 0)
101 	    {
102 		baseline = tests[i]->stats.min_ticks;
103 
104 		x = i; y = 0;
105 		cairo_matrix_transform_point (m, &x, &y);
106 		x = floor (x);
107 		y = floor (y);
108 		cairo_move_to (cr, x, y);
109 		last_y = y;
110 		break;
111 	    }
112 	}
113 
114 	for (++i; i < num_reports; i++) {
115 	    if (tests[i]->name &&
116 		test_report_cmp_backend_then_name (tests[i], min_test) == 0)
117 	    {
118 		x = i, y = tests[i]->stats.min_ticks / baseline;
119 
120 		if (y < 1.)
121 		    y = -1./y + 1;
122 		else
123 		    y -= 1;
124 
125 		cairo_matrix_transform_point (m, &x, &y);
126 		x = floor (x);
127 		y = floor (y);
128 		cairo_line_to (cr, x, last_y);
129 		cairo_line_to (cr, x, y);
130 		last_y = y;
131 	    }
132 	}
133 	{
134 	    x = num_reports, y = 0;
135 	    cairo_matrix_transform_point (m, &x, &y);
136 	    x = floor (x);
137 	    cairo_line_to (cr, x, last_y);
138 	}
139 
140 	cairo_set_line_width (cr, 1.);
141 	cairo_stroke (cr);
142 
143 	/* then draw the points */
144 	for (i = 0; i < num_reports; i++) {
145 	    if (tests[i]->name &&
146 		test_report_cmp_backend_then_name (tests[i], min_test) == 0)
147 	    {
148 		baseline = tests[i]->stats.min_ticks;
149 
150 		x = i; y = 0;
151 		cairo_matrix_transform_point (m, &x, &y);
152 		x = floor (x);
153 		y = floor (y);
154 		cairo_move_to (cr, x, y);
155 		cairo_close_path (cr);
156 		last_y = y;
157 
158 		tests[i]++;
159 		break;
160 	    }
161 	}
162 
163 	for (++i; i < num_reports; i++) {
164 	    if (tests[i]->name &&
165 		test_report_cmp_backend_then_name (tests[i], min_test) == 0)
166 	    {
167 		x = i, y = tests[i]->stats.min_ticks / baseline;
168 
169 		if (y < 1.)
170 		    y = -1./y + 1;
171 		else
172 		    y -= 1;
173 
174 		cairo_matrix_transform_point (m, &x, &y);
175 		x = floor (x);
176 		y = floor (y);
177 		cairo_move_to (cr, x, last_y);
178 		cairo_close_path (cr);
179 		cairo_move_to (cr, x, y);
180 		cairo_close_path (cr);
181 		last_y = y;
182 
183 		tests[i]++;
184 	    }
185 	}
186 	{
187 	    x = num_reports, y = 0;
188 	    cairo_matrix_transform_point (m, &x, &y);
189 	    x = floor (x);
190 	    cairo_move_to (cr, x, last_y);
191 	    cairo_close_path (cr);
192 	}
193 	cairo_set_source_rgba (cr, 0, 0, 0, .5);
194 	cairo_set_dash (cr, dots, 2, 0.);
195 	cairo_set_line_width (cr, 3.);
196 	cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
197 	cairo_stroke (cr);
198 	cairo_restore (cr);
199 
200 	cases++;
201     }
202     free (tests);
203 }
204 
205 static void
draw_hline(cairo_t * cr,const cairo_matrix_t * m,double y0,double xmin,double xmax)206 draw_hline (cairo_t		 *cr,
207 	    const cairo_matrix_t *m,
208 	    double		  y0,
209 	    double		  xmin,
210 	    double		  xmax)
211 {
212     double x, y;
213     double py_offset;
214 
215     py_offset = fmod (cairo_get_line_width (cr) / 2., 1.);
216 
217     x = xmin; y = y0;
218     cairo_matrix_transform_point (m, &x, &y);
219     cairo_move_to (cr, floor (x), floor (y) + py_offset);
220 
221     x = xmax; y = y0;
222     cairo_matrix_transform_point (m, &x, &y);
223     cairo_line_to (cr, ceil (x), floor (y) + py_offset);
224 
225     cairo_stroke (cr);
226 }
227 
228 static void
draw_label(cairo_t * cr,const cairo_matrix_t * m,double y0,double xmin,double xmax)229 draw_label (cairo_t		 *cr,
230 	    const cairo_matrix_t *m,
231 	    double		  y0,
232 	    double		  xmin,
233 	    double		  xmax)
234 {
235     double x, y;
236     char buf[80];
237     cairo_text_extents_t extents;
238 
239     snprintf (buf, sizeof (buf), "%.0fx", fabs (y0));
240     cairo_text_extents (cr, buf, &extents);
241 
242     x = xmin; y = y0;
243     cairo_matrix_transform_point (m, &x, &y);
244     cairo_move_to (cr,
245 		   x - extents.width - 4,
246 		   y - (extents.height/2. + extents.y_bearing));
247     cairo_show_text (cr, buf);
248 
249 
250     snprintf (buf, sizeof (buf), "%.0fx", fabs (y0));
251     cairo_text_extents (cr, buf, &extents);
252 
253     x = xmax; y = y0;
254     cairo_matrix_transform_point (m, &x, &y);
255     cairo_move_to (cr,
256 		   x + 4,
257 		   y - (extents.height/2. + extents.y_bearing));
258     cairo_show_text (cr, buf);
259 }
260 
261 #define ALIGN_X(v) ((v)<<0)
262 #define ALIGN_Y(v) ((v)<<2)
263 static void
draw_rotated_label(cairo_t * cr,const char * text,double x,double y,double angle,int align)264 draw_rotated_label (cairo_t    *cr,
265 		    const char *text,
266 		    double	x,
267 		    double	y,
268 		    double	angle,
269 		    int 	align)
270 {
271     cairo_text_extents_t extents;
272 
273     cairo_text_extents (cr, text, &extents);
274 
275     cairo_save (cr); {
276 	cairo_translate (cr, x, y);
277 	cairo_rotate (cr, angle);
278 	switch (align) {
279 	case ALIGN_X(0) | ALIGN_Y(0):
280 	    cairo_move_to (cr,
281 			   -extents.x_bearing,
282 			   -extents.y_bearing);
283 	    break;
284 	case ALIGN_X(0) | ALIGN_Y(1):
285 	    cairo_move_to (cr,
286 			   -extents.x_bearing,
287 			   - (extents.height/2. + extents.y_bearing));
288 	    break;
289 	case ALIGN_X(0) | ALIGN_Y(2):
290 	    cairo_move_to (cr,
291 			   -extents.x_bearing,
292 			   - (extents.height + extents.y_bearing));
293 	    break;
294 
295 	case ALIGN_X(1) | ALIGN_Y(0):
296 	    cairo_move_to (cr,
297 			   - (extents.width/2. + extents.x_bearing),
298 			   -extents.y_bearing);
299 	    break;
300 	case ALIGN_X(1) | ALIGN_Y(1):
301 	    cairo_move_to (cr,
302 			   - (extents.width/2. + extents.x_bearing),
303 			   - (extents.height/2. + extents.y_bearing));
304 	    break;
305 	case ALIGN_X(1) | ALIGN_Y(2):
306 	    cairo_move_to (cr,
307 			   - (extents.width/2. + extents.x_bearing),
308 			   - (extents.height + extents.y_bearing));
309 	    break;
310 
311 	case ALIGN_X(2) | ALIGN_Y(0):
312 	    cairo_move_to (cr,
313 			   - (extents.width + extents.x_bearing),
314 			   -extents.y_bearing);
315 	    break;
316 	case ALIGN_X(2) | ALIGN_Y(1):
317 	    cairo_move_to (cr,
318 			   - (extents.width + extents.x_bearing),
319 			   - (extents.height/2. + extents.y_bearing));
320 	    break;
321 	case ALIGN_X(2) | ALIGN_Y(2):
322 	    cairo_move_to (cr,
323 			   - (extents.width + extents.x_bearing),
324 			   - (extents.height + extents.y_bearing));
325 	    break;
326 	}
327 	cairo_show_text (cr, text);
328     } cairo_restore (cr);
329 }
330 
331 #define PAD 36
332 static void
graph_view_draw(GraphView * self,cairo_t * cr)333 graph_view_draw (GraphView *self,
334 		 cairo_t   *cr)
335 {
336     cairo_matrix_t m;
337     const double dash[2] = {4, 4};
338     double range;
339     int i;
340 
341     if (self->widget.allocation.width < 4 *PAD)
342 	return;
343     if (self->widget.allocation.height < 3 *PAD)
344 	return;
345 
346     range = floor (self->ymax+1) - ceil (self->ymin-1);
347 
348     cairo_matrix_init_translate (&m, PAD, self->widget.allocation.height - PAD);
349     cairo_matrix_scale (&m,
350 			(self->widget.allocation.width-2*PAD)/(self->num_reports),
351 			-(self->widget.allocation.height-2*PAD)/range);
352     cairo_matrix_translate (&m, 0,   floor (self->ymax+1));
353 
354     if (self->selected_report != -1) {
355 	cairo_save (cr); {
356 	    double x0, x1, y;
357 	    x0 = self->selected_report; y = 0;
358 	    cairo_matrix_transform_point (&m, &x0, &y);
359 	    x0 = floor (x0);
360 	    x1 = self->selected_report + 1; y = 0;
361 	    cairo_matrix_transform_point (&m, &x1, &y);
362 	    x1 = ceil (x1);
363 	    y = (x1 - x0) / 8;
364 	    y = MIN (y, PAD / 2);
365 	    x0 -= y;
366 	    x1 += y;
367 	    cairo_rectangle (cr, x0, PAD/2, x1-x0, self->widget.allocation.height-2*PAD + PAD);
368 	    gdk_cairo_set_source_color (cr, &self->widget.style->base[GTK_STATE_SELECTED]);
369 	    cairo_fill (cr);
370 	} cairo_restore (cr);
371     }
372 
373     cairo_save (cr); {
374 	cairo_pattern_t *linear;
375 	double x, y;
376 
377 	gdk_cairo_set_source_color (cr,
378 				    &self->widget.style->fg[GTK_WIDGET_STATE (self)]);
379 	cairo_set_line_width (cr, 2.);
380 	draw_hline (cr, &m, 0, 0, self->num_reports);
381 
382 	cairo_set_line_width (cr, 1.);
383 	cairo_set_dash (cr, NULL, 0, 0);
384 
385 	for (i = ceil (self->ymin-1); i <= floor (self->ymax+1); i++) {
386 	    if (i != 0)
387 		draw_hline (cr, &m, i, 0, self->num_reports);
388 	}
389 
390 	cairo_set_font_size (cr, 11);
391 
392 	linear = cairo_pattern_create_linear (0, PAD, 0, self->widget.allocation.height-2*PAD);
393 	cairo_pattern_add_color_stop_rgb (linear, 0, 0, 1, 0);
394 	cairo_pattern_add_color_stop_rgb (linear, 1, 1, 0, 0);
395 	cairo_set_source (cr, linear);
396 	cairo_pattern_destroy (linear);
397 
398 	for (i = ceil (self->ymin-1); i <= floor (self->ymax+1); i++) {
399 	    if (i != 0)
400 		draw_label (cr, &m, i, 0, self->num_reports);
401 	}
402 
403 	x = 0, y = floor (self->ymax+1);
404 	cairo_matrix_transform_point (&m, &x, &y);
405 	draw_rotated_label (cr, "Faster", x - 7, y + 14,
406 			    270./360 * 2 * G_PI,
407 			    ALIGN_X(2) | ALIGN_Y(1));
408 	x = self->num_reports, y = floor (self->ymax+1);
409 	cairo_matrix_transform_point (&m, &x, &y);
410 	draw_rotated_label (cr, "Faster", x + 11, y + 14,
411 			    270./360 * 2 * G_PI,
412 			    ALIGN_X(2) | ALIGN_Y(1));
413 
414 	x = 0, y = ceil (self->ymin-1);
415 	cairo_matrix_transform_point (&m, &x, &y);
416 	draw_rotated_label (cr, "Slower", x - 7, y - 14,
417 			    90./360 * 2 * G_PI,
418 			    ALIGN_X(2) | ALIGN_Y(1));
419 	x = self->num_reports, y = ceil (self->ymin-1);
420 	cairo_matrix_transform_point (&m, &x, &y);
421 	draw_rotated_label (cr, "Slower", x + 11, y - 14,
422 			    90./360 * 2 * G_PI,
423 			    ALIGN_X(2) | ALIGN_Y(1));
424     } cairo_restore (cr);
425 
426     draw_baseline_performance (self->cases,
427 			       self->reports, self->num_reports,
428 			       cr, &m);
429 
430     cairo_save (cr); {
431 	cairo_set_source_rgb (cr, 0.7, 0.7, 0.7);
432 	cairo_set_line_width (cr, 1.);
433 	cairo_set_dash (cr, dash, 2, 0);
434 	draw_hline (cr, &m, 0, 0, self->num_reports);
435     } cairo_restore (cr);
436 }
437 
438 static gboolean
graph_view_expose(GtkWidget * w,GdkEventExpose * ev)439 graph_view_expose (GtkWidget	  *w,
440 		   GdkEventExpose *ev)
441 {
442     GraphView *self = (GraphView *) w;
443     cairo_t *cr;
444 
445     cr = gdk_cairo_create (w->window);
446     gdk_cairo_set_source_color (cr, &w->style->base[GTK_WIDGET_STATE (w)]);
447     cairo_paint (cr);
448 
449     graph_view_draw (self, cr);
450 
451     cairo_destroy (cr);
452 
453     return FALSE;
454 }
455 
456 static gboolean
graph_view_button_press(GtkWidget * w,GdkEventButton * ev)457 graph_view_button_press (GtkWidget	*w,
458 			 GdkEventButton *ev)
459 {
460     GraphView *self = (GraphView *) w;
461     cairo_matrix_t m;
462     double x,y;
463     int i;
464 
465     cairo_matrix_init_translate (&m, PAD, self->widget.allocation.height-PAD);
466     cairo_matrix_scale (&m, (self->widget.allocation.width-2*PAD)/self->num_reports, -(self->widget.allocation.height-2*PAD)/(self->ymax - self->ymin));
467     cairo_matrix_translate (&m, 0, -self->ymin);
468     cairo_matrix_invert (&m);
469 
470     x = ev->x;
471     y = ev->y;
472     cairo_matrix_transform_point (&m, &x, &y);
473 
474     i = floor (x);
475     if (i < 0 || i >= self->num_reports)
476 	i = -1;
477 
478     if (i != self->selected_report) {
479 	self->selected_report = i;
480 	gtk_widget_queue_draw (w);
481 
482 	g_signal_emit (w, signals[REPORT_SELECTED], 0, i);
483     }
484 
485     return FALSE;
486 }
487 
488 static gboolean
graph_view_button_release(GtkWidget * w,GdkEventButton * ev)489 graph_view_button_release (GtkWidget	  *w,
490 			   GdkEventButton *ev)
491 {
492     GraphView *self = (GraphView *) w;
493 
494     return FALSE;
495 }
496 
497 static void
graph_view_realize(GtkWidget * widget)498 graph_view_realize (GtkWidget *widget)
499 {
500     GdkWindowAttr attributes;
501 
502     GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED);
503 
504     attributes.window_type = GDK_WINDOW_CHILD;
505     attributes.x = widget->allocation.x;
506     attributes.y = widget->allocation.y;
507     attributes.width  = widget->allocation.width;
508     attributes.height = widget->allocation.height;
509     attributes.wclass = GDK_INPUT_OUTPUT;
510     attributes.visual = gtk_widget_get_visual (widget);
511     attributes.colormap = gtk_widget_get_colormap (widget);
512     attributes.event_mask = gtk_widget_get_events (widget) |
513 			    GDK_BUTTON_PRESS_MASK |
514 			    GDK_BUTTON_RELEASE_MASK |
515 			    GDK_EXPOSURE_MASK;
516 
517     widget->window = gdk_window_new (gtk_widget_get_parent_window (widget),
518 				     &attributes,
519 				     GDK_WA_X | GDK_WA_Y |
520 				     GDK_WA_VISUAL | GDK_WA_COLORMAP);
521     gdk_window_set_user_data (widget->window, widget);
522 
523     widget->style = gtk_style_attach (widget->style, widget->window);
524     gtk_style_set_background (widget->style, widget->window, GTK_STATE_NORMAL);
525 }
526 
527 static void
graph_view_finalize(GObject * obj)528 graph_view_finalize (GObject *obj)
529 {
530     G_OBJECT_CLASS (graph_view_parent_class)->finalize (obj);
531 }
532 
533 static void
graph_view_class_init(GraphViewClass * klass)534 graph_view_class_init (GraphViewClass *klass)
535 {
536     GObjectClass *object_class = (GObjectClass *) klass;
537     GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
538 
539     object_class->finalize = graph_view_finalize;
540 
541     widget_class->realize = graph_view_realize;
542     widget_class->expose_event = graph_view_expose;
543     widget_class->button_press_event = graph_view_button_press;
544     widget_class->button_release_event = graph_view_button_release;
545 
546     signals[REPORT_SELECTED] =
547 	g_signal_new ("report-selected",
548 		      G_TYPE_FROM_CLASS (object_class),
549 		      G_SIGNAL_RUN_FIRST,
550 		      0,//G_STRUCT_OFFSET (GraphView, report_selected),
551 		      NULL, NULL,
552 		      g_cclosure_marshal_VOID__INT,
553 		      G_TYPE_NONE, 1, G_TYPE_INT);
554 }
555 
556 static void
graph_view_init(GraphView * self)557 graph_view_init (GraphView *self)
558 {
559     self->selected_report = -1;
560 }
561 
562 GtkWidget *
graph_view_new(void)563 graph_view_new (void)
564 {
565     return g_object_new (graph_view_get_type (), NULL);
566 }
567 
568 void
graph_view_update_visible(GraphView * gv)569 graph_view_update_visible (GraphView *gv)
570 {
571     double min, max;
572     test_case_t *cases;
573 
574     cases = gv->cases;
575 
576     min = max = 1.;
577     while (cases->name != NULL) {
578 	if (cases->shown) {
579 	    if (cases->min < min)
580 		min = cases->min;
581 	    if (cases->max > max)
582 		max = cases->max;
583 	}
584 	cases++;
585     }
586     gv->ymin = -1/min + 1;
587     gv->ymax = max - 1;
588 
589     gtk_widget_queue_draw (&gv->widget);
590 }
591 
592 void
graph_view_set_reports(GraphView * gv,test_case_t * cases,cairo_perf_report_t * reports,int num_reports)593 graph_view_set_reports (GraphView	    *gv,
594 			test_case_t	    *cases,
595 			cairo_perf_report_t *reports,
596 			int		     num_reports)
597 {
598     /* XXX ownership? */
599     gv->cases = cases;
600     gv->reports = reports;
601     gv->num_reports = num_reports;
602 
603     graph_view_update_visible (gv);
604 }
605