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
#define GLIB_DISABLE_DEPRECATION_WARNINGS
29

            
30
#include "cairo-perf.h"
31
#include "cairo-perf-graph.h"
32

            
33
#include <stdlib.h>
34
#include <string.h>
35
#include <errno.h>
36

            
37
#ifdef G_OS_UNIX
38
#include <unistd.h>
39
#include <fcntl.h>
40
#endif
41

            
42
#include <cairo.h>
43

            
44
static void
45
usage (const char *argv0)
46
{
47
    char const *basename = strrchr (argv0, '/');
48
    basename = basename ? basename+1 : argv0;
49
    g_printerr ("Usage: %s [options] file1 file2 [...]\n\n", basename);
50
    g_printerr ("Draws a graph illustrating the change in performance over a series.\n");
51
    exit(1);
52
}
53

            
54
enum {
55
    CASE_SHOWN,
56
    CASE_INCONSISTENT,
57
    CASE_BACKEND,
58
    CASE_CONTENT,
59
    CASE_NAME,
60
    CASE_SIZE,
61
    CASE_FG_COLOR,
62
    CASE_DATA,
63
    CASE_NCOLS
64
};
65

            
66
static GtkTreeStore *
67
cases_to_store (test_case_t *cases)
68
{
69
    GtkTreeStore *store;
70
    GtkTreeIter backend_iter;
71
    GtkTreeIter content_iter;
72
    const char *backend = NULL;
73
    const char *content = NULL;
74

            
75
    store = gtk_tree_store_new (CASE_NCOLS,
76
				G_TYPE_BOOLEAN, /* shown */
77
				G_TYPE_BOOLEAN, /* inconsistent */
78
				G_TYPE_STRING, /* backend */
79
				G_TYPE_STRING, /* content */
80
				G_TYPE_STRING, /* name */
81
				G_TYPE_INT, /* size */
82
				GDK_TYPE_COLOR, /* fg color */
83
				G_TYPE_POINTER); /* data */
84
    while (cases->backend != NULL) {
85
	GtkTreeIter iter;
86

            
87
	if (backend == NULL || strcmp (backend, cases->backend)) {
88
	    gtk_tree_store_append (store, &backend_iter, NULL);
89
	    gtk_tree_store_set (store, &backend_iter,
90
				CASE_SHOWN, TRUE,
91
				CASE_BACKEND, cases->backend,
92
				-1);
93
	    backend = cases->backend;
94
	    content = NULL;
95
	}
96
	if (content == NULL || strcmp (content, cases->content)) {
97
	    gtk_tree_store_append (store, &content_iter, &backend_iter);
98
	    gtk_tree_store_set (store, &content_iter,
99
				CASE_SHOWN, TRUE,
100
				CASE_BACKEND, cases->backend,
101
				CASE_CONTENT, cases->content,
102
				-1);
103
	    content = cases->content;
104
	}
105

            
106
	gtk_tree_store_append (store, &iter, &content_iter);
107
	gtk_tree_store_set (store, &iter,
108
			    CASE_SHOWN, TRUE,
109
			    CASE_BACKEND, cases->backend,
110
			    CASE_CONTENT, cases->content,
111
			    CASE_NAME, cases->name,
112
			    CASE_SIZE, cases->size,
113
			    CASE_FG_COLOR, &cases->color,
114
			    CASE_DATA, cases,
115
			    -1);
116
	cases++;
117
    }
118

            
119
    return store;
120
}
121

            
122
struct _app_data {
123
    GtkWidget *window;
124

            
125
    test_case_t *cases;
126
    cairo_perf_report_t *reports;
127
    int num_reports;
128

            
129
    GtkTreeStore *case_store;
130

            
131
    GIOChannel *git_io;
132
    GtkTextBuffer *git_buffer;
133

            
134
    GtkWidget *gv;
135
};
136

            
137
static void
138
recurse_set_shown (GtkTreeModel *model,
139
		   GtkTreeIter	*parent,
140
		   gboolean	 shown)
141
{
142
    GtkTreeIter iter;
143

            
144
    if (gtk_tree_model_iter_children (model, &iter, parent)) do {
145
	test_case_t *c;
146

            
147
	gtk_tree_model_get (model, &iter, CASE_DATA, &c, -1);
148
	if (c == NULL) {
149
	    recurse_set_shown (model, &iter, shown);
150
	} else if (shown != c->shown) {
151
	    c->shown = shown;
152
	    gtk_tree_store_set (GTK_TREE_STORE (model), &iter,
153
				CASE_SHOWN, shown,
154
				CASE_INCONSISTENT, FALSE,
155
				-1);
156
	}
157
    } while (gtk_tree_model_iter_next (model, &iter));
158
}
159

            
160
static gboolean
161
children_consistent (GtkTreeModel *model,
162
		     GtkTreeIter  *parent)
163
{
164
    GtkTreeIter iter;
165
    gboolean first = TRUE;
166
    gboolean first_active;
167

            
168
    if (gtk_tree_model_iter_children (model, &iter, parent)) do {
169
	gboolean active, inconsistent;
170

            
171
	gtk_tree_model_get (model, &iter,
172
			    CASE_INCONSISTENT, &inconsistent,
173
			    CASE_SHOWN, &active,
174
			    -1);
175
	if (inconsistent)
176
	    return FALSE;
177

            
178
	if (first) {
179
	    first_active = active;
180
	    first = FALSE;
181
	} else if (active != first_active)
182
	    return FALSE;
183
    } while (gtk_tree_model_iter_next (model, &iter));
184

            
185
    return TRUE;
186
}
187

            
188
static void
189
check_consistent (GtkTreeModel *model,
190
		  GtkTreeIter  *child)
191
{
192
    GtkTreeIter parent;
193

            
194
    if (gtk_tree_model_iter_parent (model, &parent, child)) {
195
	gtk_tree_store_set (GTK_TREE_STORE (model), &parent,
196
			    CASE_INCONSISTENT,
197
			    ! children_consistent (model, &parent),
198
			    -1);
199
	check_consistent (model, &parent);
200
    }
201
}
202

            
203
static void
204
show_case_toggled (GtkCellRendererToggle *cell,
205
		   gchar		 *str,
206
		   struct _app_data	 *app)
207
{
208
    GtkTreeModel *model;
209
    GtkTreePath *path;
210
    GtkTreeIter iter;
211
    test_case_t *c;
212
    gboolean active;
213

            
214
    active = ! gtk_cell_renderer_toggle_get_active (cell);
215

            
216
    model = GTK_TREE_MODEL (app->case_store);
217

            
218
    path = gtk_tree_path_new_from_string (str);
219
    gtk_tree_model_get_iter (model, &iter, path);
220
    gtk_tree_path_free (path);
221

            
222
    gtk_tree_store_set (app->case_store, &iter,
223
			CASE_SHOWN, active,
224
			CASE_INCONSISTENT, FALSE,
225
			-1);
226
    gtk_tree_model_get (model, &iter, CASE_DATA, &c, -1);
227
    if (c != NULL) {
228
	if (active == c->shown)
229
	    return;
230

            
231
	c->shown = active;
232
    } else {
233
	recurse_set_shown (model, &iter, active);
234
    }
235
    check_consistent (model, &iter);
236

            
237
    graph_view_update_visible ((GraphView *) app->gv);
238
}
239

            
240
#ifdef G_OS_UNIX
241

            
242
static gboolean
243
git_read (GIOChannel	   *io,
244
	  GIOCondition	    cond,
245
	  struct _app_data *app)
246
{
247
    int fd;
248

            
249
    fd = g_io_channel_unix_get_fd (io);
250
    do {
251
	char buf[4096];
252
	int len;
253
	GtkTextIter end;
254

            
255
	len = read (fd, buf, sizeof (buf));
256
	if (len <= 0) {
257
	    int err = len ? errno : 0;
258
	    switch (err) {
259
	    case EAGAIN:
260
	    case EINTR:
261
		return TRUE;
262
	    default:
263
		g_io_channel_unref (app->git_io);
264
		app->git_io = NULL;
265
		return FALSE;
266
	    }
267
	}
268

            
269
	gtk_text_buffer_get_end_iter (app->git_buffer, &end);
270
	gtk_text_buffer_insert (app->git_buffer, &end, buf, len);
271
    } while (TRUE);
272
}
273

            
274
static void
275
do_git (struct _app_data  *app,
276
	char		 **argv)
277
{
278
    gint output;
279
    GError *error = NULL;
280
    GtkTextIter start, stop;
281
    long flags;
282

            
283
    if (! g_spawn_async_with_pipes (NULL, argv, NULL,
284
				    G_SPAWN_SEARCH_PATH |
285
				    G_SPAWN_STDERR_TO_DEV_NULL |
286
				    G_SPAWN_FILE_AND_ARGV_ZERO,
287
				    NULL, NULL, NULL,
288
				    NULL, &output, NULL,
289
				    &error))
290
    {
291
	g_error ("spawn failed: %s", error->message);
292
    }
293

            
294
    if (app->git_io) {
295
	g_io_channel_shutdown (app->git_io, FALSE, NULL);
296
	g_io_channel_unref (app->git_io);
297
    }
298

            
299
    gtk_text_buffer_get_bounds (app->git_buffer, &start, &stop);
300
    gtk_text_buffer_delete (app->git_buffer, &start, &stop);
301

            
302
    flags = fcntl (output, F_GETFL);
303
    if ((flags & O_NONBLOCK) == 0)
304
	fcntl (output, F_SETFL, flags | O_NONBLOCK);
305

            
306
    app->git_io = g_io_channel_unix_new (output);
307
    g_io_add_watch (app->git_io, G_IO_IN | G_IO_HUP, (GIOFunc) git_read, app);
308
}
309

            
310
#endif
311

            
312
static void
313
gv_report_selected (GraphView	     *gv,
314
		    int 	      i,
315
		    struct _app_data *app)
316
{
317
    cairo_perf_report_t *report;
318
    char *hyphen;
319

            
320
    if (i == -1)
321
	return;
322

            
323
    report = &app->reports[i];
324
    hyphen = strchr (report->configuration, '-');
325
    if (hyphen != NULL) {
326
	int len = hyphen - report->configuration;
327
	char *id = g_malloc (len + 1);
328
	char *argv[5];
329

            
330
	memcpy (id, report->configuration, len);
331
	id[len] = '\0';
332

            
333
	argv[0] = (char *) "git";
334
	argv[1] = (char *) "git";
335
	argv[2] = (char *) "show";
336
	argv[3] = id;
337
	argv[4] = NULL;
338

            
339
#ifdef G_OS_UNIX
340
	do_git (app, argv);
341
#else
342
        g_print ("id: %s\n", id);
343
#endif
344
	g_free (id);
345
    }
346
}
347

            
348
static GtkWidget *
349
window_create (test_case_t	   *cases,
350
	       cairo_perf_report_t *reports,
351
	       int		    num_reports)
352
{
353
    GtkWidget *window, *table, *w;
354
    GtkWidget *tv, *sw;
355
    GtkTreeStore *store;
356
    GtkTreeViewColumn *column;
357
    GtkCellRenderer *renderer;
358
    struct _app_data *data;
359

            
360

            
361
    data = g_new0 (struct _app_data, 1);
362
    data->cases = cases;
363
    data->reports = reports;
364
    data->num_reports = num_reports;
365

            
366
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
367
    gtk_window_set_title (GTK_WINDOW (window), "Cairo Performance Graph");
368
    g_object_set_data_full (G_OBJECT (window),
369
			    "app-data", data, (GDestroyNotify)g_free);
370

            
371
    data->window = window;
372

            
373
    table = gtk_table_new (2, 2, FALSE);
374

            
375
    /* legend & show/hide lines (categorised) */
376
    tv = gtk_tree_view_new ();
377
    store = cases_to_store (cases);
378
    data->case_store = store;
379
    gtk_tree_view_set_model (GTK_TREE_VIEW (tv), GTK_TREE_MODEL (store));
380

            
381
    renderer = gtk_cell_renderer_toggle_new ();
382
    column = gtk_tree_view_column_new_with_attributes (NULL,
383
	    renderer,
384
	    "active", CASE_SHOWN,
385
	    "inconsistent", CASE_INCONSISTENT,
386
	    NULL);
387
    gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column);
388
    g_signal_connect (renderer, "toggled",
389
		      G_CALLBACK (show_case_toggled), data);
390

            
391
    renderer = gtk_cell_renderer_text_new ();
392
    column = gtk_tree_view_column_new_with_attributes ("Backend",
393
	    renderer,
394
	    "text", CASE_BACKEND,
395
	    NULL);
396
    gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column);
397

            
398
    renderer = gtk_cell_renderer_text_new ();
399
    column = gtk_tree_view_column_new_with_attributes ("Content",
400
	    renderer,
401
	    "text", CASE_CONTENT,
402
	    NULL);
403
    gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column);
404

            
405
    renderer = gtk_cell_renderer_text_new ();
406
    column = gtk_tree_view_column_new_with_attributes ("Test",
407
	    renderer,
408
	    "text", CASE_NAME,
409
	    "foreground-gdk", CASE_FG_COLOR,
410
	    NULL);
411
    gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column);
412

            
413
    renderer = gtk_cell_renderer_text_new ();
414
    column = gtk_tree_view_column_new_with_attributes ("Size",
415
	    renderer,
416
	    "text", CASE_SIZE,
417
	    NULL);
418
    gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column);
419

            
420
    gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (tv), TRUE);
421
    g_object_unref (store);
422

            
423
    sw = gtk_scrolled_window_new (NULL, NULL);
424
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
425
				    GTK_POLICY_NEVER,
426
				    GTK_POLICY_AUTOMATIC);
427
    gtk_container_add (GTK_CONTAINER (sw), tv);
428
    gtk_widget_show (tv);
429
    gtk_table_attach (GTK_TABLE (table), sw,
430
		      0, 1, 0, 2,
431
		      GTK_FILL, GTK_FILL,
432
		      4, 4);
433
    gtk_widget_show (sw);
434

            
435
    /* the performance chart */
436
    w = graph_view_new ();
437
    data->gv = w;
438
    g_signal_connect (w, "report-selected",
439
		      G_CALLBACK (gv_report_selected), data);
440
    graph_view_set_reports ((GraphView *)w, cases, reports, num_reports);
441
    gtk_table_attach (GTK_TABLE (table), w,
442
		      1, 2, 0, 1,
443
		      GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND,
444
		      4, 4);
445
    gtk_widget_show (w);
446

            
447
    /* interesting information - presumably the commit log */
448
    w = gtk_text_view_new ();
449
    data->git_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w));
450
    sw = gtk_scrolled_window_new (NULL, NULL);
451
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
452
				    GTK_POLICY_NEVER,
453
				    GTK_POLICY_AUTOMATIC);
454
    gtk_container_add (GTK_CONTAINER (sw), w);
455
    gtk_widget_show (w);
456
    gtk_table_attach (GTK_TABLE (table), sw,
457
		      1, 2, 1, 2,
458
		      GTK_FILL, GTK_FILL | GTK_EXPAND,
459
		      4, 4);
460
    gtk_widget_show (sw);
461

            
462
    gtk_container_add (GTK_CONTAINER (window), table);
463
    gtk_widget_show (table);
464

            
465
    return window;
466
}
467

            
468
static void
469
name_to_color (const char *name,
470
	       GdkColor   *color)
471
{
472
    gint v = g_str_hash (name);
473

            
474
    color->red = ((v >>  0) & 0xff) / 384. * 0xffff;
475
    color->green = ((v >>  8) & 0xff) / 384. * 0xffff;
476
    color->blue = ((v >> 16) & 0xff) / 384. * 0xffff;
477
}
478

            
479
static test_case_t *
480
test_cases_from_reports (cairo_perf_report_t *reports,
481
			 int		      num_reports)
482
{
483
    test_case_t *cases, *c;
484
    test_report_t **tests;
485
    int i, j;
486
    int num_tests;
487

            
488
    num_tests = 0;
489
    for (i = 0; i < num_reports; i++) {
490
	for (j = 0; reports[i].tests[j].name != NULL; j++)
491
	    ;
492
	if (j > num_tests)
493
	    num_tests = j;
494
    }
495

            
496
    cases = xcalloc (num_tests+1, sizeof (test_case_t));
497
    tests = xmalloc (num_reports * sizeof (test_report_t *));
498
    for (i = 0; i < num_reports; i++)
499
	tests[i] = reports[i].tests;
500

            
501
    c = cases;
502
    while (1) {
503
	int seen_non_null;
504
	test_report_t *min_test;
505

            
506
	/* We expect iterations values of 0 when multiple raw reports
507
	 * for the same test have been condensed into the stats of the
508
	 * first. So we just skip these later reports that have no
509
	 * stats. */
510
	seen_non_null = 0;
511
	for (i = 0; i < num_reports; i++) {
512
	    while (tests[i]->name && tests[i]->stats.iterations == 0)
513
		tests[i]++;
514
	    if (tests[i]->name)
515
		seen_non_null++;
516
	}
517

            
518
	if (seen_non_null < 2)
519
	    break;
520

            
521
	/* Find the minimum of all current tests, (we have to do this
522
	 * in case some reports don't have a particular test). */
523
	for (i = 0; i < num_reports; i++) {
524
	    if (tests[i]->name) {
525
		min_test = tests[i];
526
		break;
527
	    }
528
	}
529
	for (++i; i < num_reports; i++) {
530
	    if (tests[i]->name &&
531
		test_report_cmp_backend_then_name (tests[i], min_test) < 0)
532
	    {
533
		min_test = tests[i];
534
	    }
535
	}
536

            
537
	c->min_test = min_test;
538
	c->backend = min_test->backend;
539
	c->content = min_test->content;
540
	c->name = min_test->name;
541
	c->size = min_test->size;
542
	c->baseline = min_test->stats.min_ticks;
543
	c->min = c->max = 1.;
544
	c->shown = TRUE;
545
	name_to_color (c->name, &c->color);
546

            
547
	for (i = 0; i < num_reports; i++) {
548
	    if (tests[i]->name &&
549
		test_report_cmp_backend_then_name (tests[i], min_test) == 0)
550
	    {
551
		tests[i]++;
552
		break;
553
	    }
554
	}
555

            
556
	for (++i; i < num_reports; i++) {
557
	    if (tests[i]->name &&
558
		test_report_cmp_backend_then_name (tests[i], min_test) == 0)
559
	    {
560
		double v = tests[i]->stats.min_ticks / c->baseline;
561
		if (v < c->min)
562
		    c->min = v;
563
		if (v > c->max)
564
		    c->max = v;
565
		tests[i]++;
566
	    }
567
	}
568

            
569
	c++;
570
    }
571
    free (tests);
572

            
573
    return cases;
574
}
575
int
576
main (int   argc,
577
      char *argv[])
578
{
579
    cairo_perf_report_t *reports;
580
    test_case_t *cases;
581
    test_report_t *t;
582
    int i;
583
    GtkWidget *window;
584

            
585
    gtk_init (&argc, &argv);
586

            
587
    if (argc < 3)
588
	usage (argv[0]);
589

            
590
    reports = xmalloc ((argc-1) * sizeof (cairo_perf_report_t));
591
    for (i = 1; i < argc; i++ )
592
	cairo_perf_report_load (&reports[i-1], argv[i], i, NULL);
593

            
594
    cases = test_cases_from_reports (reports, argc-1);
595

            
596
    window = window_create (cases, reports, argc-1);
597
    g_signal_connect (window, "delete-event",
598
		      G_CALLBACK (gtk_main_quit), NULL);
599
    gtk_widget_show (window);
600

            
601
    gtk_main ();
602

            
603
    /* Pointless memory cleanup, (would be a great place for talloc) */
604
    free (cases);
605
    for (i = 0; i < argc-1; i++) {
606
	for (t = reports[i].tests; t->name; t++) {
607
	    free (t->samples);
608
	    free (t->backend);
609
	    free (t->name);
610
	}
611
	free (reports[i].tests);
612
	free (reports[i].configuration);
613
    }
614
    free (reports);
615

            
616
    return 0;
617
}