I was frustrated with the gap between bench.c and decent charts, so I made the attached patch. It adds a simple Python program to generate these with gnuplot. I've also attached sample output (one-active.png).

Best regards,
Scott


--
Scott Lamb <http://www.slamb.org/>

<<inline: one-active.png>>

Add a chart generator.

Also clean up what appears to be a totally extraneous error output from
bench.c.

Index: test/chartbench.py
===================================================================
--- test/chartbench.py  (revision 0)
+++ test/chartbench.py  (revision 0)
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+"""
+%prog [options]
+
+Creates libevent benchmark charts.
+
+Requirements:
+* Python >= 2.4 for the subprocess module
+* test-init and bench in current directory
+* gnuplot with png output format (and optionally freetype support)
+"""
+
+from __future__ import division
+
+import optparse
+import os
+import subprocess
+import sys
+import traceback
+
+ALL_METHODS = 'kqueue devpoll poll select epoll evport'.split(' ')
+
+STYLE_MAP = {
+    'candlesticks': 'using 1:3:2:6:5 with candlesticks linewidth 2',
+    'lines': 'using 1:4 with lines linewidth 2 smooth csplines',
+}
+
+def get_environment(method):
+    """Returns an environment that makes libevent use the specified method."""
+    env = {}
+    for i in ALL_METHODS:
+        if i != method:
+            env['EVENT_NO%s' % i.upper()] = 'yes'
+    return env
+
+def median(data):
+    """Returns the median of a sorted list of numbers."""
+    n = len(data)
+    if n % 2 == 0:
+        return (data[n//2] + data[n//2+1]) / 2
+    else:
+        return data[n//2]
+
+def run_bench(method, pipes, active, writes):
+    """Gets candestick bounds for a given test.
+
+    Returns (minimum, first quartile, median, third quartile, maximum).
+    """
+    print 'run_bench(%r, %d, %d, %d)' % (method, pipes, active, writes)
+
+    # Get the data.
+    bench = subprocess.Popen(('./bench -n %d -a %d -w %d'
+                              % (pipes, active, writes)).split(' '),
+                             bufsize=1, stdout=subprocess.PIPE,
+                             env=get_environment(method))
+    data = [float(line) for line in bench.stdout]
+    retval = bench.wait()
+    if retval != 0:
+        raise Exception('bench returned %d' % retval)
+    if not data:
+        raise Exception('bench returned insufficient data')
+
+    # Now get our statistics.
+    data.sort()
+    return (data[0],
+            median(data[0:len(data)//2]),
+            median(data),
+            median(data[len(data)//2:]),
+            data[-1])
+
+def escape(literal):
+    """Escapes the given literal for use in gnuplot control channel.
+
+    See gnuplot's "help syntax" for details."""
+    return ('"%s"' % literal.replace('\\', '\\\\')
+                            .replace('"', '\\"')
+                            .replace('\n', '\\n'))
+
+def plot(filename, title, options, methods, active, writes):
+    """Creates the plot described by the arguments."""
+
+    # Launch gnuplot. It wants to poke around at my environment, so I won't
+    # give it one. (At least if it gets an unusable $DISPLAY, it's unhappy.)
+    if options.store_commands:
+        gnuplot_out = open('%s.plot' % filename, 'w')
+    else:
+        gnuplot = subprocess.Popen(['gnuplot'], stdin=subprocess.PIPE,
+                                   env={'PATH': os.environ['PATH']})
+        gnuplot_out = gnuplot.stdin
+
+    if options.font is None:
+        gnuplot_out.write('set terminal png\n')
+    else:
+        gnuplot_out.write('set terminal png font %s %d\n'
+                          % (escape(options.font), options.fontsize))
+    gnuplot_out.write("""
+        set output %(filename)s
+        set title %(title)s
+        set xlabel "Inactive file descriptors"
+        set ylabel "Time (\xC2\xB5s)"
+        set palette color
+        set style fill solid
+        set key top left
+        set logscale y 10
+        set style line 1 lc rgb "#888888"
+        set border 3 linestyle 1
+        set xtics out nomirror textcolor rgb "#000000"
+        set ytics out nomirror textcolor rgb "#000000"
+        set grid
+    """ % {
+        'title': escape(title),
+        'filename': escape(filename),
+    })
+    gnuplot_out.write('plot [0:] [] ' +
+                        ', '.join(['"-" %s title %s'
+                                   % (STYLE_MAP[options.style], escape(method))
+                                   for method in methods]) +
+                        '\n')
+
+    # Write data for each method.
+    for method in methods:
+        for i in range(0, options.max+options.step, options.step):
+            stats = run_bench(method=method,
+                              pipes=i+active,
+                              active=active,
+                              writes=writes)
+            gnuplot_out.write('%d %g %g %g %g %g\n' % tuple((i,) + stats))
+        gnuplot_out.write('e\n')
+
+    # Finish up.
+    if not options.store_commands:
+        gnuplot_out.close()
+        retval = gnuplot.wait()
+        if retval != 0:
+            raise Exception('gnuplot failure')
+
+def main(args):
+    # Parse arguments.
+    parser = optparse.OptionParser(usage=__doc__)
+    parser.add_option('-s', '--step', dest='step', default=250, type='int',
+                      help='plot a point every STEP file descriptors')
+    parser.add_option('-m', '--max', dest='max', default=25000, type='int',
+                      help='maximum number of file descriptors')
+    parser.add_option('--style', dest='style', default='candlesticks',
+                      help='plot style', choices=STYLE_MAP.keys())
+    parser.add_option('--font', dest='font',
+                      help='path to TTF file (requires gnuplot support)')
+    parser.add_option('--font-size', dest='fontsize', default=10, type='int',
+                      help='in points; only used if FONT is specified')
+    parser.add_option('--store-commands', dest='store_commands', default=False,
+                      help='store commands rather than executing gnuplot',
+                      action='store_true')
+    options, args = parser.parse_args()
+    if len(args) != 0:
+        parser.error('No positional arguments expected')
+
+    # Determine what methods are supported.
+    supported_methods = []
+    for method in ALL_METHODS:
+        if subprocess.call('./test-init', env=get_environment(method)) == 0:
+            supported_methods.append(method)
+    if not supported_methods:
+        raise Exception('No supported methods')
+    print 'supported methods: %s' % ' '.join(supported_methods)
+
+    # Plot.
+    plot(filename='one-active.png',
+         title='1 active connection and 1 write',
+         options=options,
+         methods=supported_methods,
+         active=1,
+         writes=1)
+    plot(filename='many-active.png',
+         title='100 active connections and 1000 writes',
+         options=options,
+         methods=supported_methods,
+         active=100,
+         writes=1000)
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))

Property changes on: test/chartbench.py
___________________________________________________________________
Name: svn:executable
   + *
Name: svn:mime-type
   + text/plain
Name: svn:eol-style
   + native

Index: test/bench.c
===================================================================
--- test/bench.c        (revision 621)
+++ test/bench.c        (working copy)
@@ -102,17 +102,12 @@
 
        count = 0;
        writes = num_writes;
-       { int xcount = 0;
        gettimeofday(&ts, NULL);
        do {
                event_loop(EVLOOP_ONCE | EVLOOP_NONBLOCK);
-               xcount++;
        } while (count != fired);
        gettimeofday(&te, NULL);
 
-       if (xcount != count) fprintf(stderr, "Xcount: %d, Rcount: %d\n", 
xcount, count);
-       }
-
        evutil_timersub(&te, &ts, &te);
 
        return (&te);
_______________________________________________
Libevent-users mailing list
Libevent-users@monkey.org
http://monkeymail.org/mailman/listinfo/libevent-users

Reply via email to