diff options
author | Chris Wilson <chris@chris-wilson.co.uk> | 2008-10-16 11:56:19 +0100 |
---|---|---|
committer | Chris Wilson <chris@chris-wilson.co.uk> | 2008-10-19 09:36:53 +0100 |
commit | f2ff7944264c23cbec856be3e85f240a93184f80 (patch) | |
tree | 0c14c7d36b71bcb92bf244b6eede1eec5b4aa4ea /perf | |
parent | 41c8eefc6d432ab213f6f405c3d6346adb7f7931 (diff) |
[perf] A crude tool to visualise performance changes across a series.
Generate a cairo-perf-diff graph for a series of commits in order to be
able to identify significant commits. Still very crude, but minimally
functional.
Diffstat (limited to 'perf')
-rw-r--r-- | perf/.gitignore | 1 | ||||
-rw-r--r-- | perf/Makefile.am | 26 | ||||
-rw-r--r-- | perf/Makefile.win32 | 6 | ||||
-rwxr-xr-x | perf/cairo-perf-diff | 12 | ||||
-rw-r--r-- | perf/cairo-perf-diff-files.c | 458 | ||||
-rwxr-xr-x | perf/cairo-perf-graph | 205 | ||||
-rw-r--r-- | perf/cairo-perf-graph-files.c | 593 | ||||
-rw-r--r-- | perf/cairo-perf-graph-widget.c | 450 | ||||
-rw-r--r-- | perf/cairo-perf-graph.h | 63 | ||||
-rw-r--r-- | perf/cairo-perf-report.c | 456 | ||||
-rw-r--r-- | perf/cairo-perf.c | 1 | ||||
-rw-r--r-- | perf/cairo-perf.h | 61 | ||||
-rw-r--r-- | perf/cairo-stats.h | 8 |
13 files changed, 1862 insertions, 478 deletions
diff --git a/perf/.gitignore b/perf/.gitignore index 858e28a7b..29ec88de7 100644 --- a/perf/.gitignore +++ b/perf/.gitignore @@ -2,6 +2,7 @@ TAGS tags cairo-perf cairo-perf-diff-files +cairo-perf-graph-files valgrind-log callgrind.out.* index.html diff --git a/perf/Makefile.am b/perf/Makefile.am index 1cdcf535e..512e6d114 100644 --- a/perf/Makefile.am +++ b/perf/Makefile.am @@ -7,15 +7,19 @@ AM_CPPFLAGS = \ -I$(top_builddir)/src \ $(CAIRO_CFLAGS) -EXTRA_PROGRAMS += cairo-perf cairo-perf-diff-files +EXTRA_PROGRAMS += cairo-perf cairo-perf-diff-files cairo-perf-graph-files EXTRA_DIST += cairo-perf-diff +EXTRA_LTLIBRARIES += libcairoperf.la + +LDADD = $(top_builddir)/boilerplate/libcairoboilerplate.la \ + $(top_builddir)/src/libcairo.la \ + libcairoperf.la \ + $(CAIROPERF_LIBS) cairo_perf_SOURCES = \ cairo-perf.c \ cairo-perf.h \ cairo-perf-cover.c \ - cairo-stats.c \ - cairo-stats.h \ box-outline.c \ composite-checker.c \ fill.c \ @@ -48,14 +52,20 @@ cairo_perf_SOURCES += cairo-perf-posix.c endif endif -cairo_perf_diff_files_SOURCES = \ - cairo-perf-diff-files.c \ +libcairoperf_la_SOURCES = \ + cairo-perf-report.c \ cairo-stats.c \ cairo-stats.h -LDADD = $(top_builddir)/boilerplate/libcairoboilerplate.la \ - $(top_builddir)/src/libcairo.la \ - $(CAIROPERF_LIBS) +cairo_perf_diff_files_SOURCES = \ + cairo-perf-diff-files.c + +cairo_perf_graph_files_SOURCES = \ + cairo-perf-graph.h \ + cairo-perf-graph-files.c \ + cairo-perf-graph-widget.c +cairo_perf_graph_files_CFLAGS = @gtk_CFLAGS@ +cairo_perf_graph_files_LDADD = @gtk_LIBS@ $(LDADD) $(top_builddir)/boilerplate/libcairoboilerplate.la: $(top_builddir)/src/libcairo.la cd $(top_builddir)/boilerplate && $(MAKE) $(AM_MAKEFLAGS) libcairoboilerplate.la diff --git a/perf/Makefile.win32 b/perf/Makefile.win32 index 47e73c4e4..ef993a783 100644 --- a/perf/Makefile.win32 +++ b/perf/Makefile.win32 @@ -37,4 +37,8 @@ $(CFG)/cairo-perf.exe: $(OBJECTS) cairo-perf-diff-files: @mkdir -p $(CFG) - @$(CC) $(CFLAGS) -Fe"$@" cairo-perf-diff-files.c cairo-stats.c -link $(LDFLAGS) + @$(CC) $(CFLAGS) -Fe"$@" cairo-perf-diff-files.c cairo-perf-report.c cairo-stats.c -link $(LDFLAGS) + +cairo-perf-graph-files: + @mkdir -p $(CFG) + @$(CC) $(CFLAGS) -Fe"$@" cairo-perf-graph-files.c cairo-perf-report.c cairo-stats.c -link $(LDFLAGS) diff --git a/perf/cairo-perf-diff b/perf/cairo-perf-diff index 5094ec399..50cd1f2c6 100755 --- a/perf/cairo-perf-diff +++ b/perf/cairo-perf-diff @@ -121,9 +121,16 @@ cpu_count() { # results from a run with an equivalent src tree. rev2perf() { rev=$1 + sha=`rev2sha $rev` + src_tree_sha=`rev2sha $rev:src` + perf_tree_sha=`rev2sha HEAD:perf` + echo "$CAIRO_PERF_DIR/${sha}-${perf_tree_sha}-${src_tree_sha}.perf" +} +rev2perf_glob() { + rev=$1 src_tree_sha=`rev2sha $rev:src` perf_tree_sha=`rev2sha HEAD:perf` - echo "$CAIRO_PERF_DIR/${perf_tree_sha}-${src_tree_sha}.perf" + echo "$CAIRO_PERF_DIR/*-${perf_tree_sha}-${src_tree_sha}.perf" } # Usage: run_cairo_perf_if_not_cached <rev> <suffix> @@ -138,7 +145,8 @@ run_cairo_perf_if_not_cached() { owd=`pwd` sha=`rev2sha $rev` perf=`rev2perf $rev` - if [ -e $perf ] && [ "$force_cairo_perf" != "true" ]; then + glob=`rev2perf_glob $rev` + if [ -e $glob ] && [ "$force_cairo_perf" != "true" ]; then return 0 fi if [ ! -d $CAIRO_PERF_DIR ]; then diff --git a/perf/cairo-perf-diff-files.c b/perf/cairo-perf-diff-files.c index 9098c91cb..9b9f5f97d 100644 --- a/perf/cairo-perf-diff-files.c +++ b/perf/cairo-perf-diff-files.c @@ -27,10 +27,6 @@ #include "cairo-perf.h" -/* We use _GNU_SOURCE for getline and strndup if available. */ -#ifndef _GNU_SOURCE -# define _GNU_SOURCE -#endif #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -38,50 +34,6 @@ #include <ctype.h> #include <math.h> #include <assert.h> -#ifdef HAVE_LIBGEN_H -#include <libgen.h> -#endif - -typedef struct _test_report { - int id; - const char *configuration; - char *backend; - char *content; - char *name; - int size; - - /* The samples only exists for "raw" reports */ - cairo_perf_ticks_t *samples; - unsigned int samples_size; - unsigned int samples_count; - - /* The stats are either read directly or computed from samples. - * If the stats have not yet been computed from samples, then - * iterations will be 0. */ - cairo_stats_t stats; -} test_report_t; - -typedef struct _test_diff { - test_report_t **tests; - int num_tests; - double min; - double max; - double change; -} test_diff_t; - -typedef struct _cairo_perf_report { - char *configuration; - const char *name; - test_report_t *tests; - int tests_size; - int tests_count; -} cairo_perf_report_t; - -typedef enum { - TEST_REPORT_STATUS_SUCCESS, - TEST_REPORT_STATUS_COMMENT, - TEST_REPORT_STATUS_ERROR -} test_report_status_t; typedef struct _cairo_perf_report_options { double min_change; @@ -96,416 +48,6 @@ typedef struct _cairo_perf_diff_files_args { cairo_perf_report_options_t options; } cairo_perf_diff_files_args_t; -/* 'ssize_t' does not exist in the C standard on win32. - * We use 'ptrdiff_t', which is nearly equivalent. */ -#ifdef _MSC_VER -typedef ptrdiff_t ssize_t; -#endif - -#ifndef __USE_GNU -static ssize_t -getline (char **lineptr, size_t *n, FILE *stream); - -static char * -strndup (const char *s, size_t n); -#endif - -#ifdef _MSC_VER -static long long -strtoll(const char *nptr, char **endptr, int base); - -static char * -basename(char *path); -#endif - -/* Ad-hoc parsing, macros with a strong dependence on the calling - * context, and plenty of other ugliness is here. But at least it's - * not perl... */ -#define parse_error(...) fprintf(stderr, __VA_ARGS__); return TEST_REPORT_STATUS_ERROR; -#define skip_char(c) \ -do { \ - if (*s && *s == (c)) { \ - s++; \ - } else { \ - parse_error ("expected '%c' but found '%c'", c, *s); \ - } \ -} while (0) -#define skip_space() while (*s && (*s == ' ' || *s == '\t')) s++; -#define parse_int(result) \ -do { \ - (result) = strtol (s, &end, 10); \ - if (*s && end != s) { \ - s = end; \ - } else { \ - parse_error("expected integer but found %s", s); \ - } \ -} while (0) -#define parse_long_long(result) \ -do { \ - (result) = strtoll (s, &end, 10); \ - if (*s && end != s) { \ - s = end; \ - } else { \ - parse_error("expected integer but found %s", s); \ - } \ -} while (0) -#define parse_double(result) \ -do { \ - (result) = strtod (s, &end); \ - if (*s && end != s) { \ - s = end; \ - } else { \ - parse_error("expected floating-point value but found %s", s); \ - } \ -} while (0) -/* Here a string is simply a sequence of non-whitespace */ -#define parse_string(result) \ -do { \ - for (end = s; *end; end++) \ - if (isspace (*end)) \ - break; \ - (result) = strndup (s, end - s); \ - if ((result) == NULL) { \ - fprintf (stderr, "Out of memory.\n"); \ - exit (1); \ - } \ - s = end; \ -} while (0) - -static test_report_status_t -test_report_parse (test_report_t *report, char *line, char *configuration) -{ - char *end; - char *s = line; - cairo_bool_t is_raw = FALSE; - double min_time, median_time; - - /* The code here looks funny unless you understand that these are - * all macro calls, (and then the code just looks sick). */ - if (*s == '\n') - return TEST_REPORT_STATUS_COMMENT; - - skip_char ('['); - skip_space (); - if (*s == '#') - return TEST_REPORT_STATUS_COMMENT; - if (*s == '*') { - s++; - is_raw = TRUE; - } else { - parse_int (report->id); - } - skip_char (']'); - - skip_space (); - - report->configuration = configuration; - parse_string (report->backend); - end = strrchr (report->backend, '-'); - if (*end) - *end++ = '\0'; - report->content = end; - - skip_space (); - - parse_string (report->name); - end = strrchr (report->name, '-'); - if (*end) - *end++ = '\0'; - report->size = atoi (end); - - skip_space (); - - report->samples = NULL; - report->samples_size = 0; - report->samples_count = 0; - - if (is_raw) { - parse_double (report->stats.ticks_per_ms); - skip_space (); - - report->samples_size = 5; - report->samples = xmalloc (report->samples_size * sizeof (cairo_perf_ticks_t)); - do { - if (report->samples_count == report->samples_size) { - report->samples_size *= 2; - report->samples = xrealloc (report->samples, - report->samples_size * sizeof (cairo_perf_ticks_t)); - } - parse_long_long (report->samples[report->samples_count++]); - skip_space (); - } while (*s && *s != '\n'); - report->stats.iterations = 0; - skip_char ('\n'); - } else { - parse_double (report->stats.min_ticks); - skip_space (); - - parse_double (min_time); - report->stats.ticks_per_ms = report->stats.min_ticks / min_time; - - skip_space (); - - parse_double (median_time); - report->stats.median_ticks = median_time * report->stats.ticks_per_ms; - - skip_space (); - - parse_double (report->stats.std_dev); - report->stats.std_dev /= 100.0; - skip_char ('%'); - - skip_space (); - - parse_int (report->stats.iterations); - - skip_space (); - skip_char ('\n'); - } - - return TEST_REPORT_STATUS_SUCCESS; -} - -/* We conditionally provide a custom implementation of getline and strndup - * as needed. These aren't necessary full-fledged general purpose - * implementations. They just get the job done for our purposes. - */ -#ifndef __USE_GNU -#define POORMANS_GETLINE_BUFFER_SIZE (65536) -static ssize_t -getline (char **lineptr, size_t *n, FILE *stream) -{ - if (!*lineptr) - { - *n = POORMANS_GETLINE_BUFFER_SIZE; - *lineptr = (char *) malloc (*n); - } - - if (!fgets (*lineptr, *n, stream)) - return -1; - - if (!feof (stream) && !strchr (*lineptr, '\n')) - { - fprintf (stderr, "The poor man's implementation of getline in " - __FILE__ " needs a bigger buffer. Perhaps it's " - "time for a complete implementation of getline.\n"); - exit (0); - } - - return strlen (*lineptr); -} -#undef POORMANS_GETLINE_BUFFER_SIZE - -static char * -strndup (const char *s, size_t n) -{ - size_t len; - char *sdup; - - if (!s) - return NULL; - - len = strlen (s); - len = (n < len ? n : len); - sdup = (char *) malloc (len + 1); - if (sdup) - { - memcpy (sdup, s, len); - sdup[len] = '\0'; - } - - return sdup; -} -#endif /* ifndef __USE_GNU */ - -/* We provide hereafter a win32 implementation of the basename - * and strtoll functions which are not available otherwise. - * The basename function is fully compliant to its GNU specs. - */ -#ifdef _MSC_VER -long long -strtoll(const char *nptr, char **endptr, int base) -{ - return _atoi64(nptr); -} - -static char * -basename(char *path) -{ - char *end, *s; - - end = (path + strlen(path) - 1); - while (end && (end >= path + 1) && (*end == '/')) { - *end = '\0'; - end--; - } - - s = strrchr(path, '/'); - if (s) { - if (s == end) { - return s; - } else { - return s+1; - } - } else { - return path; - } -} -#endif /* ifndef _MSC_VER */ - -static int -test_report_cmp_backend_then_name (const void *a, const void *b) -{ - const test_report_t *a_test = a; - const test_report_t *b_test = b; - - int cmp; - - cmp = strcmp (a_test->backend, b_test->backend); - if (cmp) - return cmp; - - cmp = strcmp (a_test->content, b_test->content); - if (cmp) - return cmp; - - /* A NULL name is a list-termination marker, so force it last. */ - if (a_test->name == NULL) - if (b_test->name == NULL) - return 0; - else - return 1; - else if (b_test->name == NULL) - return -1; - - cmp = strcmp (a_test->name, b_test->name); - if (cmp) - return cmp; - - if (a_test->size < b_test->size) - return -1; - if (a_test->size > b_test->size) - return 1; - - return 0; -} - -static void -cairo_perf_report_sort_and_compute_stats (cairo_perf_report_t *report) -{ - test_report_t *base, *next, *last, *t; - - /* First we sort, since the diff needs both lists in the same - * order */ - qsort (report->tests, report->tests_count, sizeof (test_report_t), - test_report_cmp_backend_then_name); - - /* The sorting also brings all related raw reports together so we - * can condense them and compute the stats. - */ - base = &report->tests[0]; - last = &report->tests[report->tests_count - 1]; - while (base <= last) { - next = base+1; - if (next <= last) { - while (next <= last && - test_report_cmp_backend_then_name (base, next) == 0) - { - next++; - } - if (next != base) { - unsigned int new_samples_count = base->samples_count; - for (t = base + 1; t < next; t++) - new_samples_count += t->samples_count; - if (new_samples_count > base->samples_size) { - base->samples_size = new_samples_count; - base->samples = xrealloc (base->samples, - base->samples_size * sizeof (cairo_perf_ticks_t)); - } - for (t = base + 1; t < next; t++) { - memcpy (&base->samples[base->samples_count], t->samples, - t->samples_count * sizeof (cairo_perf_ticks_t)); - base->samples_count += t->samples_count; - } - } - } - if (base->samples) - _cairo_stats_compute (&base->stats, base->samples, base->samples_count); - base = next; - } -} - -static void -cairo_perf_report_load (cairo_perf_report_t *report, const char *filename) -{ - FILE *file; - test_report_status_t status; - int line_number = 0; - char *line = NULL; - size_t line_size = 0; - char *configuration; - char *dot; - char *baseName; - - configuration = xmalloc (strlen (filename) * sizeof (char) + 1); - strcpy (configuration, filename); - baseName = strdup (basename (configuration)); - report->configuration = xmalloc (strlen (filename) * sizeof (char) + 1); - strcpy(report->configuration, baseName); - free (configuration); - dot = strrchr (report->configuration, '.'); - if (dot) - *dot = '\0'; - - report->name = filename; - report->tests_size = 16; - report->tests = xmalloc (report->tests_size * sizeof (test_report_t)); - report->tests_count = 0; - - file = fopen (filename, "r"); - if (file == NULL) { - fprintf (stderr, "Failed to open %s: %s\n", - filename, strerror (errno)); - exit (1); - } - - while (1) { - if (report->tests_count == report->tests_size) { - report->tests_size *= 2; - report->tests = xrealloc (report->tests, - report->tests_size * sizeof (test_report_t)); - } - - line_number++; - if (getline (&line, &line_size, file) == -1) - break; - - status = test_report_parse (&report->tests[report->tests_count], - line, report->configuration); - if (status == TEST_REPORT_STATUS_ERROR) - fprintf (stderr, "Ignoring unrecognized line %d of %s:\n%s", - line_number, filename, line); - if (status == TEST_REPORT_STATUS_SUCCESS) - report->tests_count++; - /* Do nothing on TEST_REPORT_STATUS_COMMENT */ - } - - if (line) - free (line); - - fclose (file); - - cairo_perf_report_sort_and_compute_stats (report); - - /* Add one final report with a NULL name to terminate the list. */ - if (report->tests_count == report->tests_size) { - report->tests_size *= 2; - report->tests = xrealloc (report->tests, - report->tests_size * sizeof (test_report_t)); - } - report->tests[report->tests_count].name = NULL; -} - static int test_diff_cmp_speedup_before_slowdown (const void *a, const void *b) { diff --git a/perf/cairo-perf-graph b/perf/cairo-perf-graph new file mode 100755 index 000000000..4d2595329 --- /dev/null +++ b/perf/cairo-perf-graph @@ -0,0 +1,205 @@ +#!/bin/sh +set -e + +### +### XXX Source common functions from cairo-perf-diff +### + +usage() { + argv0=`basename $0` + + cat >&2 << END +Usage: +As opposed to its sibling, cairo-perf-diff, cairo-perf-graph targets +reviewing changes between series by graphically comparing the performance +at each commit. + +The two revisions can be any revision accepted by git. For example: + + $argv0 1.2.0 1.2.4 # Compare performance of 1.2.0 to 1.2.4 + +Options: + +-f, --force + Forces cairo-perf-diff to re-run performance tests + even if cached performance data is available. + +-h, --html + With this option performance changes are summarized + as HTML table. + +Additional options can be passed the child cairo-perf process +by separating them with a double hyphen (--). For example, to +examine what the impact of the latest change is on the stroke +test you might use: + + $argv0 HEAD -- stroke + +The performance results are cached in .perf next to the .git directory. + +Set CAIRO_AUTOGEN_OPTIONS to pass options to autogen for both +builds. +END + + exit 1 +} + +# First, pull off any known options +while true; do + case $1 in + -f|--force) force_cairo_perf="true";; + -h|--html) html_output="true";; + *) break;; + esac + + shift +done + +# Then if anything is left that still looks like an option, (begins +# with a dash), give usage to catch --help or any other -garbage +if [ $# -eq 0 ] || [ "`echo "$1" | sed 's/^-//'`" != "$1" ]; then + usage +fi + +# Finally, pick up the actual revision arguments +old="$1" +new="$2" +shift 2 + +# And post-finally, pass anything after -- on to cairo-perf +CAIRO_PERF_OPTIONS="-r -i 25" +if [ $# -gt 0 ]; then + if [ "$1" = "--" ]; then + shift 1 + CAIRO_PERF_OPTIONS="$CAIRO_PERF_OPTIONS $@" + else + usage + fi +fi + +git_setup() { + SUBDIRECTORY_OK='Yes' + . git-sh-setup + CAIRO_DIR=`dirname $GIT_DIR` + if [ "$CAIRO_DIR" = "." ]; then + CAIRO_DIR=`pwd` + fi + CAIRO_PERF_DIR=$CAIRO_DIR/.perf +} + +rev2sha() { + rev=$1 + git rev-parse --verify $rev || ( echo "Cannot resolve $rev as a git object" && exit 1 ) +} + +cpu_count() { + test -f /proc/cpuinfo && + grep -c '^processor[[:blank:]]\+:' /proc/cpuinfo || + echo 1 +} + +# We cache performance output based on a two-part name capturing the +# current performance test suite and the library being tested. We +# capture these as the tree object of the perf directory in HEAD and +# the tree object of the src directory of the revision being tested. +# +# This way, whenever the performance suite is updated, cached output +# from old versions of the suite are automatically invalidated. Also, +# if a commit just changes things outside of the src tree, (say it +# changes the "test" test suite, or README or configure.in, or +# whatever), cairo-perf-diff will be smart enough to still use cached +# results from a run with an equivalent src tree. +rev2perf() { + rev=$1 + sha=`rev2sha $rev` + src_tree_sha=`rev2sha $rev:src` + perf_tree_sha=`rev2sha HEAD:perf` + echo "$CAIRO_PERF_DIR/${sha}-${perf_tree_sha}-${src_tree_sha}.perf" +} +rev2perf_glob() { + rev=$1 + src_tree_sha=`rev2sha $rev:src` + perf_tree_sha=`rev2sha HEAD:perf` + echo "$CAIRO_PERF_DIR/*-${perf_tree_sha}-${src_tree_sha}.perf" +} + +# Usage: run_cairo_perf_if_not_cached <rev> <suffix> +# The <rev> argument must be a valid git ref-spec that can +# be resolved to a commit. The suffix is just something +# unique so that build directories can be separated for +# multiple calls to this function. +run_cairo_perf_if_not_cached() { + rev=$1 + build_dir="build-$2" + + owd=`pwd` + sha=`rev2sha $rev` + perf=`rev2perf $rev` + glob=`rev2perf_glob $rev` + if [ -e $glob ] && [ "$force_cairo_perf" != "true" ]; then + return 0 + fi + if [ ! -d $CAIRO_PERF_DIR ]; then + echo "Creating new perf cache in $CAIRO_PERF_DIR" + mkdir $CAIRO_PERF_DIR + fi + + cd $CAIRO_DIR + boilerplate_files=`git ls-tree --name-only HEAD boilerplate/*` + perf_files=`git ls-tree --name-only HEAD perf/*` + cd $CAIRO_PERF_DIR + + if [ ! -d $build_dir ]; then + git clone -s $CAIRO_DIR $build_dir + (cd $build_dir; git checkout -b tmp-cairo-perf-diff $sha) + fi + cd $build_dir + + git checkout tmp-cairo-perf-diff + git reset --hard $sha + + if [ -z "$MAKEFLAGS" ]; then + CPU_COUNT=`cpu_count` + export MAKEFLAGS="-j`expr $CPU_COUNT + 1`" + fi + + if [ ! -e Makefile ]; then + CFLAGS="-O2" ./autogen.sh $CAIRO_AUTOGEN_OPTIONS + fi + make CFLAGS="-O2" || (rm config.cache && make CFLAGS="-O2") + for file in $boilerplate_files; do + rsync $CAIRO_DIR/$file boilerplate + done + (cd boilerplate; make) + for file in $perf_files; do + rsync $CAIRO_DIR/$file perf + done + cd perf; + make cairo-perf || exit 1 + echo "Running \"cairo-perf $CAIRO_PERF_OPTIONS\" against $rev. Results will be cached in:" + echo "$perf" + (./cairo-perf $CAIRO_PERF_OPTIONS || echo "*** Performance test crashed") >> $perf + cd $owd +} + +git_setup + +# Build cairo-perf-graph-files if not available +if [ ! -e $CAIRO_DIR/perf/cairo-perf-graph-files ]; then + echo "Building cairo-perf-graph-files" + if [ "x$OS" = "xWindows_NT" ]; then + make -f Makefile.win32 -C $CAIRO_DIR/perf/ cairo-perf-graph-files CFG=debug + else + make -C $CAIRO_DIR/perf/ cairo-perf-graph-files + fi +fi + +revs="" +for rev in `git rev-list --reverse $old..$new`; do + run_cairo_perf_if_not_cached $rev rev + perf=`rev2perf $rev` + [ -e $perf ] && revs="$revs $perf" +done + +exec $CAIRO_DIR/perf/cairo-perf-graph-files $revs +#exec $CAIRO_DIR/libtool --mode=execute gdb --args $CAIRO_DIR/perf/cairo-perf-graph-files $revs diff --git a/perf/cairo-perf-graph-files.c b/perf/cairo-perf-graph-files.c new file mode 100644 index 000000000..5122b9faf --- /dev/null +++ b/perf/cairo-perf-graph-files.c @@ -0,0 +1,593 @@ +/* + * Copyright © 2008 Chris Wilson + * + * Permission to use, copy, modify, distribute, and sell this software + * and its documentation for any purpose is hereby granted without + * fee, provided that the above copyright notice appear in all copies + * and that both that copyright notice and this permission notice + * appear in supporting documentation, and that the name of the + * copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + * + * Authors: Chris Wilson <chris@chris-wilson.co.uk> + */ + +#include "cairo-perf.h" +#include "cairo-perf-graph.h" + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> + +#include <cairo.h> + +static void +usage (const char *argv0) +{ + char const *basename = strrchr (argv0, '/'); + basename = basename ? basename+1 : argv0; + g_printerr ("Usage: %s [options] file1 file2 [...]\n\n", basename); + g_printerr ("Draws a graph illustrating the change in performance over a series.\n"); + exit(1); +} + +enum { + CASE_SHOWN, + CASE_INCONSISTENT, + CASE_BACKEND, + CASE_CONTENT, + CASE_NAME, + CASE_SIZE, + CASE_FG_COLOR, + CASE_DATA, + CASE_NCOLS +}; + +static GtkTreeStore * +cases_to_store (test_case_t *cases) +{ + GtkTreeStore *store; + GtkTreeIter backend_iter; + GtkTreeIter content_iter; + const char *backend = NULL; + const char *content = NULL; + + store = gtk_tree_store_new (CASE_NCOLS, + G_TYPE_BOOLEAN, /* shown */ + G_TYPE_BOOLEAN, /* inconsistent */ + G_TYPE_STRING, /* backend */ + G_TYPE_STRING, /* content */ + G_TYPE_STRING, /* name */ + G_TYPE_INT, /* size */ + GDK_TYPE_COLOR, /* fg color */ + G_TYPE_POINTER); /* data */ + while (cases->backend != NULL) { + GtkTreeIter iter; + + if (backend == NULL || strcmp (backend, cases->backend)) { + gtk_tree_store_append (store, &backend_iter, NULL); + gtk_tree_store_set (store, &backend_iter, + CASE_SHOWN, TRUE, + CASE_BACKEND, cases->backend, + -1); + backend = cases->backend; + content = NULL; + } + if (content == NULL || strcmp (content, cases->content)) { + gtk_tree_store_append (store, &content_iter, &backend_iter); + gtk_tree_store_set (store, &content_iter, + CASE_SHOWN, TRUE, + CASE_BACKEND, cases->backend, + CASE_CONTENT, cases->content, + -1); + content = cases->content; + } + + gtk_tree_store_append (store, &iter, &content_iter); + gtk_tree_store_set (store, &iter, + CASE_SHOWN, TRUE, + CASE_BACKEND, cases->backend, + CASE_CONTENT, cases->content, + CASE_NAME, cases->name, + CASE_SIZE, cases->size, + CASE_FG_COLOR, &cases->color, + CASE_DATA, cases, + -1); + cases++; + } + + return store; +} + +struct _app_data { + GtkWidget *window; + + test_case_t *cases; + cairo_perf_report_t *reports; + int num_reports; + + GtkTreeStore *case_store; + + GIOChannel *git_io; + GtkTextBuffer *git_buffer; + + GtkWidget *gv; +}; + +static void +recurse_set_shown (GtkTreeModel *model, GtkTreeIter *parent, gboolean shown) +{ + GtkTreeIter iter; + + if (gtk_tree_model_iter_children (model, &iter, parent)) do { + test_case_t *c; + + gtk_tree_model_get (model, &iter, CASE_DATA, &c, -1); + if (c == NULL) { + recurse_set_shown (model, &iter, shown); + } else if (shown != c->shown) { + c->shown = shown; + gtk_tree_store_set (GTK_TREE_STORE (model), &iter, + CASE_SHOWN, shown, + CASE_INCONSISTENT, FALSE, + -1); + } + } while (gtk_tree_model_iter_next (model, &iter)); +} + +static gboolean +children_consistent (GtkTreeModel *model, GtkTreeIter *parent) +{ + GtkTreeIter iter; + gboolean first = TRUE; + gboolean first_active; + + if (gtk_tree_model_iter_children (model, &iter, parent)) do { + gboolean active, inconsistent; + + gtk_tree_model_get (model, &iter, + CASE_INCONSISTENT, &inconsistent, + CASE_SHOWN, &active, + -1); + if (inconsistent) + return FALSE; + + if (first) { + first_active = active; + first = FALSE; + } else if (active != first_active) + return FALSE; + } while (gtk_tree_model_iter_next (model, &iter)); + + return TRUE; +} + +static void +check_consistent (GtkTreeModel *model, GtkTreeIter *child) +{ + GtkTreeIter parent; + + if (gtk_tree_model_iter_parent (model, &parent, child)) { + gtk_tree_store_set (GTK_TREE_STORE (model), &parent, + CASE_INCONSISTENT, + ! children_consistent (model, &parent), + -1); + check_consistent (model, &parent); + } +} + +static void +show_case_toggled (GtkCellRendererToggle *cell, + gchar *str, + struct _app_data *app) +{ + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + test_case_t *c; + gboolean active; + + active = ! gtk_cell_renderer_toggle_get_active (cell); + + model = GTK_TREE_MODEL (app->case_store); + + path = gtk_tree_path_new_from_string (str); + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + + gtk_tree_store_set (app->case_store, &iter, + CASE_SHOWN, active, + CASE_INCONSISTENT, FALSE, + -1); + gtk_tree_model_get (model, &iter, CASE_DATA, &c, -1); + if (c != NULL) { + if (active == c->shown) + return; + + c->shown = active; + } else { + recurse_set_shown (model, &iter, active); + } + check_consistent (model, &iter); + + graph_view_update_visible ((GraphView *) app->gv); +} + +static gboolean +git_read (GIOChannel *io, GIOCondition cond, struct _app_data *app) +{ + int fd; + + fd = g_io_channel_unix_get_fd (io); + do { + char buf[4096]; + int len; + GtkTextIter end; + + len = read (fd, buf, sizeof (buf)); + if (len <= 0) { + int err = len ? errno : 0; + switch (err) { + case EAGAIN: + case EINTR: + return TRUE; + default: + g_io_channel_unref (app->git_io); + app->git_io = NULL; + return FALSE; + } + } + + gtk_text_buffer_get_end_iter (app->git_buffer, &end); + gtk_text_buffer_insert (app->git_buffer, &end, buf, len); + } while (TRUE); +} + +static void +do_git (struct _app_data *app, char **argv) +{ + gint output; + GError *error = NULL; + GtkTextIter start, stop; + long flags; + + if (! g_spawn_async_with_pipes (NULL, argv, NULL, + G_SPAWN_SEARCH_PATH | + G_SPAWN_STDERR_TO_DEV_NULL | + G_SPAWN_FILE_AND_ARGV_ZERO, + NULL, NULL, NULL, + NULL, &output, NULL, + &error)) + { + g_error ("spawn failed: %s", error->message); + } + + if (app->git_io) { + g_io_channel_shutdown (app->git_io, FALSE, NULL); + g_io_channel_unref (app->git_io); + } + + gtk_text_buffer_get_bounds (app->git_buffer, &start, &stop); + gtk_text_buffer_delete (app->git_buffer, &start, &stop); + + flags = fcntl (output, F_GETFL); + if ((flags & O_NONBLOCK) == 0) + fcntl (output, F_SETFL, flags | O_NONBLOCK); + + app->git_io = g_io_channel_unix_new (output); + g_io_add_watch (app->git_io, G_IO_IN | G_IO_HUP, (GIOFunc) git_read, app); +} + +static void +gv_report_selected (GraphView *gv, int i, struct _app_data *app) +{ + cairo_perf_report_t *report; + char *hyphen; + + if (i == -1) + return; + + report = &app->reports[i]; + hyphen = strchr (report->configuration, '-'); + if (hyphen != NULL) { + int len = hyphen - report->configuration; + char *id = g_malloc (len + 1); + char *argv[5]; + + memcpy (id, report->configuration, len); + id[len] = '\0'; + + argv[0] = (char *) "git"; + argv[1] = (char *) "git"; + argv[2] = (char *) "show"; + argv[3] = id; + argv[4] = NULL; + + do_git (app, argv); + free (id); + } +} + +static GtkWidget * +window_create (test_case_t *cases, + cairo_perf_report_t *reports, + int num_reports) +{ + GtkWidget *window, *table, *w; + GtkWidget *tv, *sw; + GtkTreeStore *store; + GtkTreeViewColumn *column; + GtkCellRenderer *renderer; + struct _app_data *data; + + + data = g_new0 (struct _app_data, 1); + data->cases = cases; + data->reports = reports; + data->num_reports = num_reports; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title (GTK_WINDOW (window), "Cairo Performance Graph"); + g_object_set_data_full (G_OBJECT (window), + "app-data", data, (GDestroyNotify)g_free); + + data->window = window; + + table = gtk_table_new (2, 2, FALSE); + + /* legend & show/hide lines (categorised) */ + tv = gtk_tree_view_new (); + store = cases_to_store (cases); + data->case_store = store; + gtk_tree_view_set_model (GTK_TREE_VIEW (tv), GTK_TREE_MODEL (store)); + + renderer = gtk_cell_renderer_toggle_new (); + column = gtk_tree_view_column_new_with_attributes (NULL, + renderer, + "active", CASE_SHOWN, + "inconsistent", CASE_INCONSISTENT, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column); + g_signal_connect (renderer, "toggled", + G_CALLBACK (show_case_toggled), data); + + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ("Backend", + renderer, + "text", CASE_BACKEND, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column); + + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ("Content", + renderer, + "text", CASE_CONTENT, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column); + + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ("Test", + renderer, + "text", CASE_NAME, + "foreground-gdk", CASE_FG_COLOR, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column); + + renderer = gtk_cell_renderer_text_new (); + column = gtk_tree_view_column_new_with_attributes ("Size", + renderer, + "text", CASE_SIZE, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column); + + gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (tv), TRUE); + g_object_unref (store); + + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_container_add (GTK_CONTAINER (sw), tv); + gtk_widget_show (tv); + gtk_table_attach (GTK_TABLE (table), sw, + 0, 1, 0, 2, + GTK_FILL, GTK_FILL, + 4, 4); + gtk_widget_show (sw); + + /* the performance chart */ + w = graph_view_new (); + data->gv = w; + g_signal_connect (w, "report-selected", + G_CALLBACK (gv_report_selected), data); + graph_view_set_reports ((GraphView *)w, cases, reports, num_reports); + gtk_table_attach (GTK_TABLE (table), w, + 1, 2, 0, 1, + GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, + 4, 4); + gtk_widget_show (w); + + /* interesting information - presumably the commit log */ + w = gtk_text_view_new (); + data->git_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w)); + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_container_add (GTK_CONTAINER (sw), w); + gtk_widget_show (w); + gtk_table_attach (GTK_TABLE (table), sw, + 1, 2, 1, 2, + GTK_FILL, GTK_FILL | GTK_EXPAND, + 4, 4); + gtk_widget_show (sw); + + gtk_container_add (GTK_CONTAINER (window), table); + gtk_widget_show (table); + + return window; +} + +static void +name_to_color (const char *name, GdkColor *color) +{ + gint v = g_str_hash (name); + + color->red = ((v >> 0) & 0xff) / 384. * 0xffff; + color->green = ((v >> 8) & 0xff) / 384. * 0xffff; + color->blue = ((v >> 16) & 0xff) / 384. * 0xffff; +} + +static test_case_t * +test_cases_from_reports (cairo_perf_report_t *reports, + int num_reports) +{ + test_case_t *cases, *c; + test_report_t **tests; + int i, j; + int num_tests; + + num_tests = 0; + for (i = 0; i < num_reports; i++) { + for (j = 0; reports[i].tests[j].name != NULL; j++) + ; + if (j > num_tests) + num_tests = j; + } + + cases = xcalloc (num_tests+1, sizeof (test_case_t)); + tests = xmalloc (num_reports * sizeof (test_report_t *)); + for (i = 0; i < num_reports; i++) + tests[i] = reports[i].tests; + + c = cases; + while (1) { + int seen_non_null; + test_report_t *min_test; + + /* We expect iterations values of 0 when multiple raw reports + * for the same test have been condensed into the stats of the + * first. So we just skip these later reports that have no + * stats. */ + seen_non_null = 0; + for (i = 0; i < num_reports; i++) { + while (tests[i]->name && tests[i]->stats.iterations == 0) + tests[i]++; + if (tests[i]->name) + seen_non_null++; + } + + if (seen_non_null < 2) + break; + + /* Find the minimum of all current tests, (we have to do this + * in case some reports don't have a particular test). */ + for (i = 0; i < num_reports; i++) { + if (tests[i]->name) { + min_test = tests[i]; + break; + } + } + for (++i; i < num_reports; i++) { + if (tests[i]->name && + test_report_cmp_backend_then_name (tests[i], min_test) < 0) + { + min_test = tests[i]; + } + } + + c->min_test = min_test; + c->backend = min_test->backend; + c->content = min_test->content; + c->name = min_test->name; + c->size = min_test->size; + c->baseline = min_test->stats.min_ticks; + c->min = c->max = 1.; + c->shown = TRUE; + name_to_color (c->name, &c->color); + + for (i = 0; i < num_reports; i++) { + if (tests[i]->name && + test_report_cmp_backend_then_name (tests[i], min_test) == 0) + { + tests[i]++; + break; + } + } + + for (++i; i < num_reports; i++) { + if (tests[i]->name && + test_report_cmp_backend_then_name (tests[i], min_test) == 0) + { + double v = tests[i]->stats.min_ticks / c->baseline; + if (v < c->min) + c->min = v; + if (v > c->max) + c->max = v; + tests[i]++; + } + } + + c++; + } + free (tests); + + return cases; +} +int +main (int argc, char *argv[]) +{ + cairo_perf_report_t *reports; + test_case_t *cases; + test_report_t *t; + int i; + GtkWidget *window; + + gtk_init (&argc, &argv); + + if (argc < 3) + usage (argv[0]); + + reports = xmalloc ((argc-1) * sizeof (cairo_perf_report_t)); + for (i = 1; i < argc; i++ ) + cairo_perf_report_load (&reports[i-1], argv[i]); + + cases = test_cases_from_reports (reports, argc-1); + + window = window_create (cases, reports, argc-1); + g_signal_connect (window, "delete-event", + G_CALLBACK (gtk_main_quit), NULL); + gtk_widget_show (window); + + gtk_main (); + + /* Pointless memory cleanup, (would be a great place for talloc) */ + free (cases); + for (i = 0; i < argc-1; i++) { + for (t = reports[i].tests; t->name; t++) { + free (t->samples); + free (t->backend); + free (t->name); + } + free (reports[i].tests); + free (reports[i].configuration); + } + free (reports); + + return 0; +} diff --git a/perf/cairo-perf-graph-widget.c b/perf/cairo-perf-graph-widget.c new file mode 100644 index 000000000..2bb48a4aa --- /dev/null +++ b/perf/cairo-perf-graph-widget.c @@ -0,0 +1,450 @@ +/* + * Copyright © 2008 Chris Wilson + * + * Permission to use, copy, modify, distribute, and sell this software + * and its documentation for any purpose is hereby granted without + * fee, provided that the above copyright notice appear in all copies + * and that both that copyright notice and this permission notice + * appear in supporting documentation, and that the name of the + * copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + * + * Authors: Chris Wilson <chris@chris-wilson.co.uk> + */ + +#include "cairo-perf.h" +#include "cairo-perf-graph.h" + +#include <gtk/gtk.h> + +struct _GraphView { + GtkWidget widget; + + test_case_t *cases; + cairo_perf_report_t *reports; + int num_reports; + double ymin, ymax; + + int selected_report; +}; + +typedef struct _GraphViewClass { + GtkWidgetClass parent_class; +} GraphViewClass; + +static GType graph_view_get_type (void); + +enum { + REPORT_SELECTED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (GraphView, graph_view, GTK_TYPE_WIDGET) + +static void +draw_baseline_performance (test_case_t *cases, + cairo_perf_report_t *reports, + int num_reports, + cairo_t *cr, + const cairo_matrix_t *m) +{ + test_report_t **tests; + double dots[2] = { 0, 1.}; + int i; + + tests = xmalloc (num_reports * sizeof (test_report_t *)); + for (i = 0; i < num_reports; i++) + tests[i] = reports[i].tests; + + while (cases->backend != NULL) { + test_report_t *min_test; + double baseline, last_y; + double x, y; + + if (! cases->shown) { + cases++; + continue; + } + + min_test = cases->min_test; + + for (i = 0; i < num_reports; i++) { + while (tests[i]->name && + test_report_cmp_backend_then_name (tests[i], min_test) < 0) + { + tests[i]++; + } + } + + /* first the stroke */ + cairo_save (cr); + cairo_set_line_width (cr, 2.); + gdk_cairo_set_source_color (cr, &cases->color); + for (i = 0; i < num_reports; i++) { + if (tests[i]->name && + test_report_cmp_backend_then_name (tests[i], min_test) == 0) + { + baseline = tests[i]->stats.min_ticks; + + x = i; y = 0; + cairo_matrix_transform_point (m, &x, &y); + x = floor (x); + y = floor (y); + cairo_move_to (cr, x, y); + last_y = y; + break; + } + } + + for (++i; i < num_reports; i++) { + if (tests[i]->name && + test_report_cmp_backend_then_name (tests[i], min_test) == 0) + { + x = i, y = tests[i]->stats.min_ticks / baseline; + + if (y < 1.) + y = -1./y + 1; + else + y -= 1; + + cairo_matrix_transform_point (m, &x, &y); + x = floor (x); + y = floor (y); + cairo_line_to (cr, x, last_y); + cairo_line_to (cr, x, y); + last_y = y; + } + } + { + x = num_reports, y = 0; + cairo_matrix_transform_point (m, &x, &y); + x = floor (x); + cairo_line_to (cr, x, last_y); + } + + cairo_set_line_width (cr, 1.); + cairo_stroke (cr); + + /* then draw the points */ + for (i = 0; i < num_reports; i++) { + if (tests[i]->name && + test_report_cmp_backend_then_name (tests[i], min_test) == 0) + { + baseline = tests[i]->stats.min_ticks; + + x = i; y = 0; + cairo_matrix_transform_point (m, &x, &y); + x = floor (x); + y = floor (y); + cairo_move_to (cr, x, y); + cairo_close_path (cr); + last_y = y; + + tests[i]++; + break; + } + } + + for (++i; i < num_reports; i++) { + if (tests[i]->name && + test_report_cmp_backend_then_name (tests[i], min_test) == 0) + { + x = i, y = tests[i]->stats.min_ticks / baseline; + + if (y < 1.) + y = -1./y + 1; + else + y -= 1; + + cairo_matrix_transform_point (m, &x, &y); + x = floor (x); + y = floor (y); + cairo_move_to (cr, x, last_y); + cairo_close_path (cr); + cairo_move_to (cr, x, y); + cairo_close_path (cr); + last_y = y; + + tests[i]++; + } + } + { + x = num_reports, y = 0; + cairo_matrix_transform_point (m, &x, &y); + x = floor (x); + cairo_move_to (cr, x, last_y); + cairo_close_path (cr); + } + cairo_set_source_rgba (cr, 0, 0, 0, .5); + cairo_set_dash (cr, dots, 2, 0.); + cairo_set_line_width (cr, 3.); + cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND); + cairo_stroke (cr); + cairo_restore (cr); + + cases++; + } + free (tests); +} + +static void +draw_hline (cairo_t *cr, const cairo_matrix_t *m, double y0, double xmin, double xmax) +{ + double x, y; + double py_offset; + + py_offset = fmod (cairo_get_line_width (cr) / 2., 1.); + + x = xmin; y = y0; + cairo_matrix_transform_point (m, &x, &y); + cairo_move_to (cr, floor (x), floor (y) + py_offset); + + x = xmax; y = y0; + cairo_matrix_transform_point (m, &x, &y); + cairo_line_to (cr, ceil (x), floor (y) + py_offset); + + cairo_stroke (cr); +} + +#define PAD 24 +static void +graph_view_draw (GraphView *self, cairo_t *cr) +{ + cairo_matrix_t m; + const double dash[2] = {4, 4}; + double range; + int i; + + range = ceil (self->ymax) - floor (self->ymin); + + cairo_matrix_init_translate (&m, PAD, PAD); + cairo_matrix_scale (&m, (self->widget.allocation.width-2*PAD)/self->num_reports, (self->widget.allocation.height-2*PAD)/range); + cairo_matrix_translate (&m, 0, - floor (self->ymin)); + + if (self->selected_report != -1) { + cairo_save (cr); { + double x0, x1, y; + x0 = self->selected_report; y = 0; + cairo_matrix_transform_point (&m, &x0, &y); + x0 = floor (x0); + x1 = self->selected_report + 1; y = 0; + cairo_matrix_transform_point (&m, &x1, &y); + x1 = ceil (x1); + y = (x1 - x0) / 8; + y = MIN (y, PAD / 2); + x0 -= y; + x1 += y; + cairo_rectangle (cr, x0, PAD/2, x1-x0, self->widget.allocation.height-2*PAD + PAD); + gdk_cairo_set_source_color (cr, &self->widget.style->base[GTK_STATE_SELECTED]); + cairo_fill (cr); + } cairo_restore (cr); + } + + cairo_save (cr); { + cairo_set_source_rgb (cr, 0.7, 0.7, 0.7); + cairo_set_line_width (cr, 2.); + draw_hline (cr, &m, 0, 0, self->num_reports-1); + + cairo_set_line_width (cr, 1.); + cairo_set_dash (cr, NULL, 0, 0); + for (i = floor (self->ymin); i <= floor (self->ymax); i++) { + if (i != 0) + draw_hline (cr, &m, i, 0, self->num_reports); + } + } cairo_restore (cr); + + draw_baseline_performance (self->cases, + self->reports, self->num_reports, + cr, &m); + + cairo_save (cr); { + cairo_set_source_rgb (cr, 0.7, 0.7, 0.7); + cairo_set_line_width (cr, 1.); + cairo_set_dash (cr, dash, 2, 0); + draw_hline (cr, &m, 0, 0, self->num_reports-1); + } cairo_restore (cr); + +} + +static gboolean +graph_view_expose (GtkWidget *w, GdkEventExpose *ev) +{ + GraphView *self = (GraphView *) w; + cairo_t *cr; + + cr = gdk_cairo_create (w->window); + gdk_cairo_region (cr, ev->region); + cairo_clip (cr); + + gdk_cairo_set_source_color (cr, &w->style->base[GTK_WIDGET_STATE (w)]); + cairo_paint (cr); + + graph_view_draw (self, cr); + + cairo_destroy (cr); + + return FALSE; +} + +static gboolean +graph_view_button_press (GtkWidget *w, GdkEventButton *ev) +{ + GraphView *self = (GraphView *) w; + cairo_matrix_t m; + double x,y; + int i; + + cairo_matrix_init_translate (&m, PAD, self->widget.allocation.height-PAD); + cairo_matrix_scale (&m, (self->widget.allocation.width-2*PAD)/self->num_reports, -(self->widget.allocation.height-2*PAD)/(self->ymax - self->ymin)); + cairo_matrix_translate (&m, 0, -self->ymin); + cairo_matrix_invert (&m); + + x = ev->x; + y = ev->y; + cairo_matrix_transform_point (&m, &x, &y); + + i = floor (x); + if (i < 0 || i >= self->num_reports) + i = -1; + + if (i != self->selected_report) { + self->selected_report = i; + gtk_widget_queue_draw (w); + + g_signal_emit (w, signals[REPORT_SELECTED], 0, i); + } + + return FALSE; +} + +static gboolean +graph_view_button_release (GtkWidget *w, GdkEventButton *ev) +{ + GraphView *self = (GraphView *) w; + + return FALSE; +} + +static void +graph_view_realize (GtkWidget *widget) +{ + GdkWindowAttr attributes; + + GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED); + + attributes.window_type = GDK_WINDOW_CHILD; + attributes.x = widget->allocation.x; + attributes.y = widget->allocation.y; + attributes.width = widget->allocation.width; + attributes.height = widget->allocation.height; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual (widget); + attributes.colormap = gtk_widget_get_colormap (widget); + attributes.event_mask = gtk_widget_get_events (widget) | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_EXPOSURE_MASK; + + widget->window = gdk_window_new (gtk_widget_get_parent_window (widget), + &attributes, + GDK_WA_X | GDK_WA_Y | + GDK_WA_VISUAL | GDK_WA_COLORMAP); + gdk_window_set_user_data (widget->window, widget); + + widget->style = gtk_style_attach (widget->style, widget->window); + gtk_style_set_background (widget->style, widget->window, GTK_STATE_NORMAL); +} + +static void +graph_view_finalize (GObject *obj) +{ + G_OBJECT_CLASS (graph_view_parent_class)->finalize (obj); +} + +static void +graph_view_class_init (GraphViewClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + GtkWidgetClass *widget_class = (GtkWidgetClass *) klass; + + object_class->finalize = graph_view_finalize; + + widget_class->realize = graph_view_realize; + widget_class->expose_event = graph_view_expose; + widget_class->button_press_event = graph_view_button_press; + widget_class->button_release_event = graph_view_button_release; + + signals[REPORT_SELECTED] = + g_signal_new ("report-selected", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_FIRST, + 0,//G_STRUCT_OFFSET (GraphView, report_selected), + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, G_TYPE_INT); +} + +static void +graph_view_init (GraphView *self) +{ + self->selected_report = -1; +} + +GtkWidget * +graph_view_new (void) +{ + return g_object_new (graph_view_get_type (), NULL); +} + +void +graph_view_update_visible (GraphView *gv) +{ + double min, max; + test_case_t *cases; + + cases = gv->cases; + + min = max = 1.; + while (cases->name != NULL) { + if (cases->shown) { + if (cases->min < min) + min = cases->min; + if (cases->max > max) + max = cases->max; + } + cases++; + } + gv->ymin = -1/min + 1; + gv->ymax = max - 1; + + gtk_widget_queue_draw (&gv->widget); +} + +void +graph_view_set_reports (GraphView *gv, + test_case_t *cases, + cairo_perf_report_t *reports, + int num_reports) +{ + /* XXX ownership? */ + gv->cases = cases; + gv->reports = reports; + gv->num_reports = num_reports; + + graph_view_update_visible (gv); +} diff --git a/perf/cairo-perf-graph.h b/perf/cairo-perf-graph.h new file mode 100644 index 000000000..62bf30fce --- /dev/null +++ b/perf/cairo-perf-graph.h @@ -0,0 +1,63 @@ +/* + * Copyright © 2008 Chris Wilson + * + * Permission to use, copy, modify, distribute, and sell this software + * and its documentation for any purpose is hereby granted without + * fee, provided that the above copyright notice appear in all copies + * and that both that copyright notice and this permission notice + * appear in supporting documentation, and that the name of the + * copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + * + * Authors: Chris Wilson <chris@chris-wilson.co.uk> + */ + +#ifndef CAIRO_PERF_GRAPH_H +#define CAIRO_PERF_GRAPH_H + +#include <gtk/gtk.h> + +#include "cairo-perf.h" + +typedef struct _test_case { + const char *backend; + const char *content; + const char *name; + int size; + + test_report_t *min_test; + + cairo_bool_t shown; + double baseline; + double min, max; + GdkColor color; +} test_case_t; + +typedef struct _GraphView GraphView; + +GtkWidget * +graph_view_new (void); + +void +graph_view_set_reports (GraphView *gv, + test_case_t *tests, + cairo_perf_report_t *reports, + int num_reports); + +void +graph_view_update_visible (GraphView *gv); + +#endif diff --git a/perf/cairo-perf-report.c b/perf/cairo-perf-report.c new file mode 100644 index 000000000..79e2f923e --- /dev/null +++ b/perf/cairo-perf-report.c @@ -0,0 +1,456 @@ +/* + * Copyright © 2006 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software + * and its documentation for any purpose is hereby granted without + * fee, provided that the above copyright notice appear in all copies + * and that both that copyright notice and this permission notice + * appear in supporting documentation, and that the name of the + * copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + * + * Authors: Carl Worth <cworth@cworth.org> + */ + +#include "cairo-perf.h" +#include "cairo-stats.h" + +/* We use _GNU_SOURCE for getline and strndup if available. */ +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <ctype.h> +#include <math.h> +#include <assert.h> +#ifdef HAVE_LIBGEN_H +#include <libgen.h> +#endif + +/* 'ssize_t' does not exist in the C standard on win32. + * We use 'ptrdiff_t', which is nearly equivalent. */ +#ifdef _MSC_VER +typedef ptrdiff_t ssize_t; +#endif + +#ifndef __USE_GNU +static ssize_t +getline (char **lineptr, size_t *n, FILE *stream); + +static char * +strndup (const char *s, size_t n); +#endif + +#ifdef _MSC_VER +static long long +strtoll(const char *nptr, char **endptr, int base); + +static char * +basename(char *path); +#endif + +/* Ad-hoc parsing, macros with a strong dependence on the calling + * context, and plenty of other ugliness is here. But at least it's + * not perl... */ +#define parse_error(...) fprintf(stderr, __VA_ARGS__); return TEST_REPORT_STATUS_ERROR; +#define skip_char(c) \ +do { \ + if (*s && *s == (c)) { \ + s++; \ + } else { \ + parse_error ("expected '%c' but found '%c'", c, *s); \ + } \ +} while (0) +#define skip_space() while (*s && (*s == ' ' || *s == '\t')) s++; +#define parse_int(result) \ +do { \ + (result) = strtol (s, &end, 10); \ + if (*s && end != s) { \ + s = end; \ + } else { \ + parse_error("expected integer but found %s", s); \ + } \ +} while (0) +#define parse_long_long(result) \ +do { \ + (result) = strtoll (s, &end, 10); \ + if (*s && end != s) { \ + s = end; \ + } else { \ + parse_error("expected integer but found %s", s); \ + } \ +} while (0) +#define parse_double(result) \ +do { \ + (result) = strtod (s, &end); \ + if (*s && end != s) { \ + s = end; \ + } else { \ + parse_error("expected floating-point value but found %s", s); \ + } \ +} while (0) +/* Here a string is simply a sequence of non-whitespace */ +#define parse_string(result) \ +do { \ + for (end = s; *end; end++) \ + if (isspace (*end)) \ + break; \ + (result) = strndup (s, end - s); \ + if ((result) == NULL) { \ + fprintf (stderr, "Out of memory.\n"); \ + exit (1); \ + } \ + s = end; \ +} while (0) + +static test_report_status_t +test_report_parse (test_report_t *report, char *line, char *configuration) +{ + char *end; + char *s = line; + cairo_bool_t is_raw = FALSE; + double min_time, median_time; + + /* The code here looks funny unless you understand that these are + * all macro calls, (and then the code just looks sick). */ + if (*s == '\n') + return TEST_REPORT_STATUS_COMMENT; + + skip_char ('['); + skip_space (); + if (*s == '#') + return TEST_REPORT_STATUS_COMMENT; + if (*s == '*') { + s++; + is_raw = TRUE; + } else { + parse_int (report->id); + } + skip_char (']'); + + skip_space (); + + report->configuration = configuration; + parse_string (report->backend); + end = strrchr (report->backend, '-'); + if (*end) + *end++ = '\0'; + report->content = end; + + skip_space (); + + parse_string (report->name); + end = strrchr (report->name, '-'); + if (*end) + *end++ = '\0'; + report->size = atoi (end); + + skip_space (); + + report->samples = NULL; + report->samples_size = 0; + report->samples_count = 0; + + if (is_raw) { + parse_double (report->stats.ticks_per_ms); + skip_space (); + + report->samples_size = 5; + report->samples = xmalloc (report->samples_size * sizeof (cairo_perf_ticks_t)); + do { + if (report->samples_count == report->samples_size) { + report->samples_size *= 2; + report->samples = xrealloc (report->samples, + report->samples_size * sizeof (cairo_perf_ticks_t)); + } + parse_long_long (report->samples[report->samples_count++]); + skip_space (); + } while (*s && *s != '\n'); + report->stats.iterations = 0; + skip_char ('\n'); + } else { + parse_double (report->stats.min_ticks); + skip_space (); + + parse_double (min_time); + report->stats.ticks_per_ms = report->stats.min_ticks / min_time; + + skip_space (); + + parse_double (median_time); + report->stats.median_ticks = median_time * report->stats.ticks_per_ms; + + skip_space (); + + parse_double (report->stats.std_dev); + report->stats.std_dev /= 100.0; + skip_char ('%'); + + skip_space (); + + parse_int (report->stats.iterations); + + skip_space (); + skip_char ('\n'); + } + + return TEST_REPORT_STATUS_SUCCESS; +} + +/* We conditionally provide a custom implementation of getline and strndup + * as needed. These aren't necessary full-fledged general purpose + * implementations. They just get the job done for our purposes. + */ +#ifndef __USE_GNU +#define POORMANS_GETLINE_BUFFER_SIZE (65536) +static ssize_t +getline (char **lineptr, size_t *n, FILE *stream) +{ + if (!*lineptr) + { + *n = POORMANS_GETLINE_BUFFER_SIZE; + *lineptr = (char *) malloc (*n); + } + + if (!fgets (*lineptr, *n, stream)) + return -1; + + if (!feof (stream) && !strchr (*lineptr, '\n')) + { + fprintf (stderr, "The poor man's implementation of getline in " + __FILE__ " needs a bigger buffer. Perhaps it's " + "time for a complete implementation of getline.\n"); + exit (0); + } + + return strlen (*lineptr); +} +#undef POORMANS_GETLINE_BUFFER_SIZE + +static char * +strndup (const char *s, size_t n) +{ + size_t len; + char *sdup; + + if (!s) + return NULL; + + len = strlen (s); + len = (n < len ? n : len); + sdup = (char *) malloc (len + 1); + if (sdup) + { + memcpy (sdup, s, len); + sdup[len] = '\0'; + } + + return sdup; +} +#endif /* ifndef __USE_GNU */ + +/* We provide hereafter a win32 implementation of the basename + * and strtoll functions which are not available otherwise. + * The basename function is fully compliant to its GNU specs. + */ +#ifdef _MSC_VER +long long +strtoll(const char *nptr, char **endptr, int base) +{ + return _atoi64(nptr); +} + +static char * +basename(char *path) +{ + char *end, *s; + + end = (path + strlen(path) - 1); + while (end && (end >= path + 1) && (*end == '/')) { + *end = '\0'; + end--; + } + + s = strrchr(path, '/'); + if (s) { + if (s == end) { + return s; + } else { + return s+1; + } + } else { + return path; + } +} +#endif /* ifndef _MSC_VER */ + +int +test_report_cmp_backend_then_name (const void *a, const void *b) +{ + const test_report_t *a_test = a; + const test_report_t *b_test = b; + + int cmp; + + cmp = strcmp (a_test->backend, b_test->backend); + if (cmp) + return cmp; + + cmp = strcmp (a_test->content, b_test->content); + if (cmp) + return cmp; + + /* A NULL name is a list-termination marker, so force it last. */ + if (a_test->name == NULL) + if (b_test->name == NULL) + return 0; + else + return 1; + else if (b_test->name == NULL) + return -1; + + cmp = strcmp (a_test->name, b_test->name); + if (cmp) + return cmp; + + if (a_test->size < b_test->size) + return -1; + if (a_test->size > b_test->size) + return 1; + + return 0; +} + +void +cairo_perf_report_sort_and_compute_stats (cairo_perf_report_t *report) +{ + test_report_t *base, *next, *last, *t; + + /* First we sort, since the diff needs both lists in the same + * order */ + qsort (report->tests, report->tests_count, sizeof (test_report_t), + test_report_cmp_backend_then_name); + + /* The sorting also brings all related raw reports together so we + * can condense them and compute the stats. + */ + base = &report->tests[0]; + last = &report->tests[report->tests_count - 1]; + while (base <= last) { + next = base+1; + if (next <= last) { + while (next <= last && + test_report_cmp_backend_then_name (base, next) == 0) + { + next++; + } + if (next != base) { + unsigned int new_samples_count = base->samples_count; + for (t = base + 1; t < next; t++) + new_samples_count += t->samples_count; + if (new_samples_count > base->samples_size) { + base->samples_size = new_samples_count; + base->samples = xrealloc (base->samples, + base->samples_size * sizeof (cairo_perf_ticks_t)); + } + for (t = base + 1; t < next; t++) { + memcpy (&base->samples[base->samples_count], t->samples, + t->samples_count * sizeof (cairo_perf_ticks_t)); + base->samples_count += t->samples_count; + } + } + } + if (base->samples) + _cairo_stats_compute (&base->stats, base->samples, base->samples_count); + base = next; + } +} + +void +cairo_perf_report_load (cairo_perf_report_t *report, + const char *filename) +{ + FILE *file; + test_report_status_t status; + int line_number = 0; + char *line = NULL; + size_t line_size = 0; + char *configuration; + char *dot; + char *baseName; + + configuration = xmalloc (strlen (filename) * sizeof (char) + 1); + strcpy (configuration, filename); + baseName = strdup (basename (configuration)); + report->configuration = xmalloc (strlen (filename) * sizeof (char) + 1); + strcpy(report->configuration, baseName); + free (configuration); + dot = strrchr (report->configuration, '.'); + if (dot) + *dot = '\0'; + + report->name = filename; + report->tests_size = 16; + report->tests = xmalloc (report->tests_size * sizeof (test_report_t)); + report->tests_count = 0; + + file = fopen (filename, "r"); + if (file == NULL) { + fprintf (stderr, "Failed to open %s: %s\n", + filename, strerror (errno)); + exit (1); + } + + while (1) { + if (report->tests_count == report->tests_size) { + report->tests_size *= 2; + report->tests = xrealloc (report->tests, + report->tests_size * sizeof (test_report_t)); + } + + line_number++; + if (getline (&line, &line_size, file) == -1) + break; + + status = test_report_parse (&report->tests[report->tests_count], + line, report->configuration); + if (status == TEST_REPORT_STATUS_ERROR) + fprintf (stderr, "Ignoring unrecognized line %d of %s:\n%s", + line_number, filename, line); + if (status == TEST_REPORT_STATUS_SUCCESS) + report->tests_count++; + /* Do nothing on TEST_REPORT_STATUS_COMMENT */ + } + + if (line) + free (line); + + fclose (file); + + cairo_perf_report_sort_and_compute_stats (report); + + /* Add one final report with a NULL name to terminate the list. */ + if (report->tests_count == report->tests_size) { + report->tests_size *= 2; + report->tests = xrealloc (report->tests, + report->tests_size * sizeof (test_report_t)); + } + report->tests[report->tests_count].name = NULL; +} + diff --git a/perf/cairo-perf.c b/perf/cairo-perf.c index 68e145bbd..613d15cf9 100644 --- a/perf/cairo-perf.c +++ b/perf/cairo-perf.c @@ -29,6 +29,7 @@ #define _GNU_SOURCE 1 /* for sched_getaffinity() */ #include "cairo-perf.h" +#include "cairo-stats.h" #include "cairo-boilerplate-getopt.h" diff --git a/perf/cairo-perf.h b/perf/cairo-perf.h index 4a75dc110..57fe85f11 100644 --- a/perf/cairo-perf.h +++ b/perf/cairo-perf.h @@ -32,7 +32,13 @@ typedef uint64_t cairo_perf_ticks_t; -#include "cairo-stats.h" +typedef struct _cairo_stats { + cairo_perf_ticks_t min_ticks; + cairo_perf_ticks_t median_ticks; + double ticks_per_ms; + double std_dev; + int iterations; +} cairo_stats_t; /* timers */ @@ -93,6 +99,59 @@ cairo_perf_cover_sources_and_operators (cairo_perf_t *perf, const char *name, cairo_perf_func_t perf_func); +/* reporter convenience routines */ + +typedef struct _test_report { + int id; + const char *configuration; + char *backend; + char *content; + char *name; + int size; + + /* The samples only exists for "raw" reports */ + cairo_perf_ticks_t *samples; + unsigned int samples_size; + unsigned int samples_count; + + /* The stats are either read directly or computed from samples. + * If the stats have not yet been computed from samples, then + * iterations will be 0. */ + cairo_stats_t stats; +} test_report_t; + +typedef struct _test_diff { + test_report_t **tests; + int num_tests; + double min; + double max; + double change; +} test_diff_t; + +typedef struct _cairo_perf_report { + char *configuration; + const char *name; + test_report_t *tests; + int tests_size; + int tests_count; +} cairo_perf_report_t; + +typedef enum { + TEST_REPORT_STATUS_SUCCESS, + TEST_REPORT_STATUS_COMMENT, + TEST_REPORT_STATUS_ERROR +} test_report_status_t; + +void +cairo_perf_report_load (cairo_perf_report_t *report, + const char *filename); + +void +cairo_perf_report_sort_and_compute_stats (cairo_perf_report_t *report); + +int +test_report_cmp_backend_then_name (const void *a, const void *b); + #define CAIRO_PERF_DECL(func) void (func) (cairo_perf_t *perf, cairo_t *cr, int width, int height) CAIRO_PERF_DECL (fill); diff --git a/perf/cairo-stats.h b/perf/cairo-stats.h index 3f8a988d9..58ff0b6f2 100644 --- a/perf/cairo-stats.h +++ b/perf/cairo-stats.h @@ -28,14 +28,6 @@ #include "cairo-perf.h" -typedef struct _cairo_stats { - cairo_perf_ticks_t min_ticks; - cairo_perf_ticks_t median_ticks; - double ticks_per_ms; - double std_dev; - int iterations; -} cairo_stats_t; - void _cairo_stats_compute (cairo_stats_t *stats, cairo_perf_ticks_t *values, |