1 /*
2  * Copyright © 2006 Red Hat, Inc.
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: Carl Worth <cworth@cworth.org>
26  */
27 
28 #define _GETDELIM 1/* for getline() on AIX */
29 
30 #include "cairo-perf.h"
31 #include "cairo-missing.h"
32 #include "cairo-stats.h"
33 
34 /* We use _GNU_SOURCE for getline and strndup if available. */
35 #ifndef _GNU_SOURCE
36 # define _GNU_SOURCE
37 #endif
38 #include <stdio.h>
39 #include <stdlib.h>
40 #include <string.h>
41 #include <errno.h>
42 #include <ctype.h>
43 #include <math.h>
44 #include <assert.h>
45 #ifdef HAVE_LIBGEN_H
46 #include <libgen.h>
47 #endif
48 
49 #ifdef _MSC_VER
50 static long long
51 strtoll (const char  *nptr,
52 	 char	    **endptr,
53 	 int	      base);
54 
55 static char *
56 basename (char *path);
57 #endif
58 
59 /* Ad-hoc parsing, macros with a strong dependence on the calling
60  * context, and plenty of other ugliness is here.  But at least it's
61  * not perl... */
62 #define parse_error(...) fprintf(stderr, __VA_ARGS__); return TEST_REPORT_STATUS_ERROR;
63 #define skip_char(c)							\
64 do {									\
65     if (*s && *s == (c)) {						\
66 	s++;								\
67     } else {								\
68 	 parse_error ("expected '%c' but found '%c'", c, *s);		\
69     }									\
70 } while (0)
71 #define skip_space() while (*s && (*s == ' ' || *s == '\t')) s++;
72 #define parse_int(result)						\
73 do {									\
74     (result) = strtol (s, &end, 10);					\
75     if (*s && end != s) {						\
76 	s = end;							\
77     } else {								\
78 	parse_error("expected integer but found %s", s);		\
79     }									\
80 } while (0)
81 #define parse_long_long(result) 					\
82 do {									\
83     (result) = strtoll (s, &end, 10);					\
84     if (*s && end != s) {						\
85 	s = end;							\
86     } else {								\
87 	parse_error("expected integer but found %s", s);		\
88     }									\
89 } while (0)
90 #define parse_double(result)						\
91 do {									\
92     (result) = strtod (s, &end);					\
93     if (*s && end != s) {						\
94 	s = end;							\
95     } else {								\
96 	parse_error("expected floating-point value but found %s", s);	\
97     }									\
98 } while (0)
99 /* Here a string is simply a sequence of non-whitespace */
100 #define parse_string(result)						\
101 do {									\
102     for (end = s; *end; end++)						\
103 	if (isspace (*end))						\
104 	    break;							\
105     (result) = strndup (s, end - s);					\
106     if ((result) == NULL) {						\
107 	fprintf (stderr, "Out of memory.\n");				\
108 	exit (1);							\
109     }									\
110     s = end;								\
111 } while (0)
112 
113 static test_report_status_t
test_report_parse(test_report_t * report,int fileno,char * line,char * configuration)114 test_report_parse (test_report_t *report,
115 		   int fileno,
116 		   char 	 *line,
117 		   char 	 *configuration)
118 {
119     char *end;
120     char *s = line;
121     cairo_bool_t is_raw = FALSE;
122     double min_time, median_time;
123 
124     /* The code here looks funny unless you understand that these are
125      * all macro calls, (and then the code just looks sick). */
126     if (*s == '\n')
127 	return TEST_REPORT_STATUS_COMMENT;
128 
129     skip_char ('[');
130     skip_space ();
131     if (*s == '#')
132 	return TEST_REPORT_STATUS_COMMENT;
133     if (*s == '*') {
134 	s++;
135 	is_raw = TRUE;
136     } else {
137 	parse_int (report->id);
138     }
139     skip_char (']');
140 
141     skip_space ();
142 
143     report->fileno = fileno;
144     report->configuration = configuration;
145     parse_string (report->backend);
146     end = strrchr (report->backend, '.');
147     if (end)
148 	*end++ = '\0';
149     report->content = end ? end : xstrdup ("???");
150 
151     skip_space ();
152 
153     parse_string (report->name);
154     end = strrchr (report->name, '.');
155     if (end)
156 	*end++ = '\0';
157     report->size = end ? atoi (end) : 0;
158 
159     skip_space ();
160 
161     report->samples = NULL;
162     report->samples_size = 0;
163     report->samples_count = 0;
164 
165     if (is_raw) {
166 	parse_double (report->stats.ticks_per_ms);
167 	skip_space ();
168 
169 	report->samples_size = 5;
170 	report->samples = xmalloc (report->samples_size * sizeof (cairo_time_t));
171 	report->stats.min_ticks = 0;
172 	do {
173 	    if (report->samples_count == report->samples_size) {
174 		report->samples_size *= 2;
175 		report->samples = xrealloc (report->samples,
176 					    report->samples_size * sizeof (cairo_time_t));
177 	    }
178 	    parse_long_long (report->samples[report->samples_count]);
179 	    if (report->samples_count == 0) {
180 		report->stats.min_ticks =
181 		    report->samples[report->samples_count];
182 	    } else if (report->stats.min_ticks >
183 		       report->samples[report->samples_count]){
184 		report->stats.min_ticks =
185 		    report->samples[report->samples_count];
186 	    }
187 	    report->samples_count++;
188 	    skip_space ();
189 	} while (*s && *s != '\n');
190 	report->stats.iterations = 0;
191 	if (*s) skip_char ('\n');
192     } else {
193 	parse_double (report->stats.min_ticks);
194 	skip_space ();
195 
196 	parse_double (min_time);
197 	report->stats.ticks_per_ms = report->stats.min_ticks / min_time;
198 
199 	skip_space ();
200 
201 	parse_double (median_time);
202 	report->stats.median_ticks = median_time * report->stats.ticks_per_ms;
203 
204 	skip_space ();
205 
206 	parse_double (report->stats.std_dev);
207 	report->stats.std_dev /= 100.0;
208 	skip_char ('%');
209 
210 	skip_space ();
211 
212 	parse_int (report->stats.iterations);
213 
214 	skip_space ();
215 	skip_char ('\n');
216     }
217 
218     return TEST_REPORT_STATUS_SUCCESS;
219 }
220 
221 /* We provide hereafter a win32 implementation of the basename
222  * and strtoll functions which are not available otherwise.
223  * The basename function is fully compliant to its GNU specs.
224  */
225 #ifdef _MSC_VER
226 long long
strtoll(const char * nptr,char ** endptr,int base)227 strtoll (const char  *nptr,
228 	 char	    **endptr,
229 	 int	      base)
230 {
231     return _atoi64(nptr);
232 }
233 
234 static char *
basename(char * path)235 basename (char *path)
236 {
237     char *end, *s;
238 
239     end = (path + strlen(path) - 1);
240     while (end && (end >= path + 1) && (*end == '/')) {
241 	*end = '\0';
242 	end--;
243     }
244 
245     s = strrchr(path, '/');
246     if (s) {
247 	if (s == end) {
248 	    return s;
249 	} else {
250 	    return s+1;
251 	}
252     } else {
253 	return path;
254     }
255 }
256 #endif /* ifndef _MSC_VER */
257 
258 int
test_report_cmp_backend_then_name(const void * a,const void * b)259 test_report_cmp_backend_then_name (const void *a,
260 				   const void *b)
261 {
262     const test_report_t *a_test = a;
263     const test_report_t *b_test = b;
264 
265     int cmp;
266 
267     cmp = strcmp (a_test->backend, b_test->backend);
268     if (cmp)
269 	return cmp;
270 
271     cmp = strcmp (a_test->content, b_test->content);
272     if (cmp)
273 	return cmp;
274 
275     /* A NULL name is a list-termination marker, so force it last. */
276     if (a_test->name == NULL)
277 	if (b_test->name == NULL)
278 	    return 0;
279 	else
280 	    return 1;
281     else if (b_test->name == NULL)
282 	return -1;
283 
284     cmp = strcmp (a_test->name, b_test->name);
285     if (cmp)
286 	return cmp;
287 
288     if (a_test->size < b_test->size)
289 	return -1;
290     if (a_test->size > b_test->size)
291 	return 1;
292 
293     return 0;
294 }
295 
296 int
test_report_cmp_name(const void * a,const void * b)297 test_report_cmp_name (const void *a,
298 		      const void *b)
299 {
300     const test_report_t *a_test = a;
301     const test_report_t *b_test = b;
302 
303     int cmp;
304 
305     /* A NULL name is a list-termination marker, so force it last. */
306     if (a_test->name == NULL)
307 	if (b_test->name == NULL)
308 	    return 0;
309 	else
310 	    return 1;
311     else if (b_test->name == NULL)
312 	return -1;
313 
314     cmp = strcmp (a_test->name, b_test->name);
315     if (cmp)
316 	return cmp;
317 
318     if (a_test->size < b_test->size)
319 	return -1;
320     if (a_test->size > b_test->size)
321 	return 1;
322 
323     return 0;
324 }
325 
326 void
cairo_perf_report_sort_and_compute_stats(cairo_perf_report_t * report,int (* cmp)(const void *,const void *))327 cairo_perf_report_sort_and_compute_stats (cairo_perf_report_t *report,
328 					  int (*cmp) (const void*, const void*))
329 {
330     test_report_t *base, *next, *last, *t;
331 
332     if (cmp == NULL)
333 	cmp = test_report_cmp_backend_then_name;
334 
335     /* First we sort, since the diff needs both lists in the same
336      * order */
337     qsort (report->tests, report->tests_count, sizeof (test_report_t), cmp);
338 
339     /* The sorting also brings all related raw reports together so we
340      * can condense them and compute the stats.
341      */
342     base = &report->tests[0];
343     last = &report->tests[report->tests_count - 1];
344     while (base <= last) {
345 	next = base+1;
346 	if (next <= last) {
347 	    while (next <= last &&
348 		   test_report_cmp_backend_then_name (base, next) == 0)
349 	    {
350 		next++;
351 	    }
352 	    if (next != base) {
353 		unsigned int new_samples_count = base->samples_count;
354 		for (t = base + 1; t < next; t++)
355 		    new_samples_count += t->samples_count;
356 		if (new_samples_count > base->samples_size) {
357 		    base->samples_size = new_samples_count;
358 		    base->samples = xrealloc (base->samples,
359 					      base->samples_size * sizeof (cairo_time_t));
360 		}
361 		for (t = base + 1; t < next; t++) {
362 		    memcpy (&base->samples[base->samples_count], t->samples,
363 			    t->samples_count * sizeof (cairo_time_t));
364 		    base->samples_count += t->samples_count;
365 		}
366 	    }
367 	}
368 	if (base->samples)
369 	    _cairo_stats_compute (&base->stats, base->samples, base->samples_count);
370 	base = next;
371     }
372 }
373 
374 void
cairo_perf_report_load(cairo_perf_report_t * report,const char * filename,int id,int (* cmp)(const void *,const void *))375 cairo_perf_report_load (cairo_perf_report_t *report,
376 			const char *filename, int id,
377 			int (*cmp) (const void *, const void *))
378 {
379     FILE *file;
380     test_report_status_t status;
381     int line_number = 0;
382     char *line = NULL;
383     size_t line_size = 0;
384     char *configuration;
385     char *dot;
386     char *baseName;
387     const char *name;
388 
389     name = filename;
390     if (name == NULL)
391 	name = "stdin";
392 
393     configuration = xstrdup (name);
394     baseName = basename (configuration);
395     report->configuration = xstrdup (baseName);
396     free (configuration);
397 
398     dot = strrchr (report->configuration, '.');
399     if (dot)
400 	*dot = '\0';
401 
402     report->name = name;
403     report->tests_size = 16;
404     report->tests = xmalloc (report->tests_size * sizeof (test_report_t));
405     report->tests_count = 0;
406     report->fileno = id;
407 
408     if (filename == NULL) {
409 	file = stdin;
410     } else {
411 	file = fopen (filename, "r");
412 	if (file == NULL) {
413 	    fprintf (stderr, "Failed to open %s: %s\n",
414 		     filename, strerror (errno));
415 	    exit (1);
416 	}
417     }
418 
419     while (1) {
420 	if (report->tests_count == report->tests_size) {
421 	    report->tests_size *= 2;
422 	    report->tests = xrealloc (report->tests,
423 				      report->tests_size * sizeof (test_report_t));
424 	}
425 
426 	line_number++;
427 	if (getline (&line, &line_size, file) == -1)
428 	    break;
429 
430 	status = test_report_parse (&report->tests[report->tests_count],
431 				    id, line, report->configuration);
432 	if (status == TEST_REPORT_STATUS_ERROR)
433 	    fprintf (stderr, "Ignoring unrecognized line %d of %s:\n%s",
434 		     line_number, filename, line);
435 	if (status == TEST_REPORT_STATUS_SUCCESS)
436 	    report->tests_count++;
437 	/* Do nothing on TEST_REPORT_STATUS_COMMENT */
438     }
439 
440     free (line);
441 
442     if (filename != NULL)
443 	fclose (file);
444 
445     cairo_perf_report_sort_and_compute_stats (report, cmp);
446 
447     /* Add one final report with a NULL name to terminate the list. */
448     if (report->tests_count == report->tests_size) {
449 	report->tests_size *= 2;
450 	report->tests = xrealloc (report->tests,
451 				  report->tests_size * sizeof (test_report_t));
452     }
453     report->tests[report->tests_count].name = NULL;
454 }
455