Index: uspace/app/perf/Makefile
===================================================================
--- uspace/app/perf/Makefile	(revision 043d464fc984ff0747f267473f19a93b734f3d8d)
+++ uspace/app/perf/Makefile	(revision 60029dfe5895b620ab28744ecb20cab6de3b9b8b)
@@ -35,4 +35,5 @@
 SOURCES = \
 	benchlist.c \
+	csv.c \
 	perf.c \
 	ipc/ns_ping.c \
Index: uspace/app/perf/csv.c
===================================================================
--- uspace/app/perf/csv.c	(revision 60029dfe5895b620ab28744ecb20cab6de3b9b8b)
+++ uspace/app/perf/csv.c	(revision 60029dfe5895b620ab28744ecb20cab6de3b9b8b)
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2019 Vojtech Horky
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup perf
+ * @{
+ */
+/**
+ * @file
+ */
+
+#include <stdlib.h>
+#include "csv.h"
+
+static FILE *csv_output = NULL;
+
+/** Open CSV benchmark report.
+ *
+ * @param filename Filename where to store the CSV.
+ * @return Whether it was possible to open the file.
+ */
+errno_t csv_report_open(const char *filename)
+{
+	csv_output = fopen(filename, "w");
+	if (csv_output == NULL) {
+		return errno;
+	}
+
+	fprintf(csv_output, "benchmark,run,size,duration_nanos\n");
+
+	return EOK;
+}
+
+/** Add one entry to the report.
+ *
+ * When csv_report_open() was not called or failed, the function does
+ * nothing.
+ *
+ * @param stopwatch Performance data of the entry.
+ * @param run_index Run index, use negative values for warm-up.
+ * @param bench Benchmark information.
+ * @param workload_size Workload size.
+ */
+void csv_report_add_entry(stopwatch_t *stopwatch, int run_index,
+    benchmark_t *bench, uint64_t workload_size)
+{
+	if (csv_output == NULL) {
+		return;
+	}
+
+	fprintf(csv_output, "%s,%d,%" PRIu64 ",%lld\n",
+	    bench->name, run_index, workload_size,
+	    (long long) stopwatch_get_nanos(stopwatch));
+}
+
+/** Close CSV report.
+ *
+ * When csv_report_open() was not called or failed, the function does
+ * nothing.
+ */
+void csv_report_close(void)
+{
+	if (csv_output != NULL) {
+		fclose(csv_output);
+	}
+}
+
+/** @}
+ */
Index: uspace/app/perf/csv.h
===================================================================
--- uspace/app/perf/csv.h	(revision 60029dfe5895b620ab28744ecb20cab6de3b9b8b)
+++ uspace/app/perf/csv.h	(revision 60029dfe5895b620ab28744ecb20cab6de3b9b8b)
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2019 Vojtech Horky
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup perf
+ * @{
+ */
+/** @file
+ */
+
+#ifndef CSV_H_
+#define CSV_H_
+
+#include <errno.h>
+#include <stdio.h>
+#include <perf.h>
+#include "perf.h"
+
+extern errno_t csv_report_open(const char *);
+extern void csv_report_add_entry(stopwatch_t *, int, benchmark_t *, uint64_t);
+extern void csv_report_close(void);
+
+#endif
+
+/** @}
+ */
Index: uspace/app/perf/perf.c
===================================================================
--- uspace/app/perf/perf.c	(revision 043d464fc984ff0747f267473f19a93b734f3d8d)
+++ uspace/app/perf/perf.c	(revision 60029dfe5895b620ab28744ecb20cab6de3b9b8b)
@@ -36,4 +36,5 @@
 
 #include <assert.h>
+#include <getopt.h>
 #include <math.h>
 #include <stdio.h>
@@ -43,8 +44,10 @@
 #include <time.h>
 #include <errno.h>
+#include <str_error.h>
 #include <perf.h>
 #include <types/casting.h>
+#include "benchlist.h"
+#include "csv.h"
 #include "perf.h"
-#include "benchlist.h"
 
 #define MIN_DURATION_SECS 10
@@ -55,4 +58,6 @@
     benchmark_t *bench, uint64_t workload_size)
 {
+	csv_report_add_entry(stopwatch, run_index, bench, workload_size);
+
 	usec_t duration_usec = NSEC2USEC(stopwatch_get_nanos(stopwatch));
 
@@ -282,30 +287,85 @@
 
 	for (size_t i = 0; i < benchmark_count; i++)
-		printf("%-*s %s\n", (int) len, benchmarks[i]->name, benchmarks[i]->desc);
-
-	printf("%-*s Run all benchmarks\n", (int) len, "*");
+		printf("  %-*s %s\n", (int) len, benchmarks[i]->name, benchmarks[i]->desc);
+
+	printf("  %-*s Run all benchmarks\n", (int) len, "*");
+}
+
+static void print_usage(const char *progname)
+{
+	printf("Usage: %s [options] <benchmark>\n", progname);
+	printf("-h, --help                 "
+	    "Print this help and exit\n");
+	printf("-o, --output filename.csv  "
+	    "Store machine-readable data in filename.csv\n");
+	printf("<benchmark> is one of the following:\n");
+	list_benchmarks();
 }
 
 int main(int argc, char *argv[])
 {
-	if (argc < 2) {
-		printf("Usage:\n\n");
-		printf("%s <benchmark>\n\n", argv[0]);
-		list_benchmarks();
-		return 0;
-	}
-
-	if (str_cmp(argv[1], "*") == 0) {
-		return run_benchmarks();
-	}
-
-	for (size_t i = 0; i < benchmark_count; i++) {
-		if (str_cmp(argv[1], benchmarks[i]->name) == 0) {
-			return (run_benchmark(benchmarks[i]) ? 0 : -1);
-		}
-	}
-
-	printf("Unknown benchmark \"%s\"\n", argv[1]);
-	return -2;
+	const char *short_options = "ho:";
+	struct option long_options[] = {
+		{ "help", optional_argument, NULL, 'h' },
+		{ "output", required_argument, NULL, 'o' },
+		{ 0, 0, NULL, 0 }
+	};
+
+	char *csv_output_filename = NULL;
+
+	int opt = 0;
+	while ((opt = getopt_long(argc, argv, short_options, long_options, NULL)) > 0) {
+		switch (opt) {
+		case 'h':
+			print_usage(*argv);
+			return 0;
+		case 'o':
+			csv_output_filename = optarg;
+			break;
+		case -1:
+		default:
+			break;
+		}
+	}
+
+	if (optind + 1 != argc) {
+		print_usage(*argv);
+		fprintf(stderr, "Error: specify one benchmark to run or * for all.\n");
+		return -3;
+	}
+
+	const char *benchmark = argv[optind];
+
+	if (csv_output_filename != NULL) {
+		errno_t rc = csv_report_open(csv_output_filename);
+		if (rc != EOK) {
+			fprintf(stderr, "Failed to open CSV report '%s': %s\n",
+			    csv_output_filename, str_error(rc));
+			return -4;
+		}
+	}
+
+	int exit_code = 0;
+
+	if (str_cmp(benchmark, "*") == 0) {
+		exit_code = run_benchmarks();
+	} else {
+		bool benchmark_exists = false;
+		for (size_t i = 0; i < benchmark_count; i++) {
+			if (str_cmp(benchmark, benchmarks[i]->name) == 0) {
+				benchmark_exists = true;
+				exit_code = run_benchmark(benchmarks[i]) ? 0 : -1;
+				break;
+			}
+		}
+		if (!benchmark_exists) {
+			printf("Unknown benchmark \"%s\"\n", benchmark);
+			exit_code = -2;
+		}
+	}
+
+	csv_report_close();
+
+	return exit_code;
 }
 
