Update GUI to attempt to graphically handle ETA output

The whole layout will probably be changed, but for now it
demonstrates how to properly integrate with the net client
to handle the data and output it.

Signed-off-by: Jens Axboe <axboe@kernel.dk>
diff --git a/client.c b/client.c
index faa990b..842f225 100644
--- a/client.c
+++ b/client.c
@@ -21,13 +21,6 @@
 #include "flist.h"
 #include "hash.h"
 
-extern void (*update_thread_status)(char *status_message, double perc);
-
-struct client_eta {
-	struct jobs_eta eta;
-	unsigned int pending;
-};
-
 static void fio_client_text_op(struct fio_client *client,
 		FILE *f, __u16 pdu_len, const char *buf)
 {
@@ -56,7 +49,6 @@
 	.group_stats	= handle_gs,
 	.eta		= handle_eta,
 	.probe		= handle_probe,
-	/* status display, if NULL, printf is used */
 };
 
 static struct timeval eta_tv;
@@ -85,9 +77,6 @@
 #define FIO_CLIENT_HASH_MASK	(FIO_CLIENT_HASH_SZ - 1)
 static struct flist_head client_hash[FIO_CLIENT_HASH_SZ];
 
-static int handle_client(struct fio_client *client, struct client_ops *ops);
-static void dec_jobs_eta(struct client_eta *eta);
-
 static void fio_client_add_hash(struct fio_client *client)
 {
 	int bucket = hash_long(client->fd, FIO_CLIENT_HASH_BITS);
@@ -135,7 +124,7 @@
 
 	if (!flist_empty(&client->eta_list)) {
 		flist_del_init(&client->eta_list);
-		dec_jobs_eta(client->eta_in_flight);
+		fio_client_dec_jobs_eta(client->eta_in_flight, display_thread_status);
 	}
 
 	free(client->hostname);
@@ -681,7 +670,7 @@
 	print_disk_util(&du->dus, &du->agg, terse_output);
 }
 
-static void convert_jobs_eta(struct jobs_eta *je)
+void fio_client_convert_jobs_eta(struct jobs_eta *je)
 {
 	int i;
 
@@ -689,12 +678,12 @@
 	je->nr_ramp		= le32_to_cpu(je->nr_ramp);
 	je->nr_pending		= le32_to_cpu(je->nr_pending);
 	je->files_open		= le32_to_cpu(je->files_open);
-	je->m_rate		= le32_to_cpu(je->m_rate);
-	je->t_rate		= le32_to_cpu(je->t_rate);
-	je->m_iops		= le32_to_cpu(je->m_iops);
-	je->t_iops		= le32_to_cpu(je->t_iops);
 
 	for (i = 0; i < 2; i++) {
+		je->m_rate[i]		= le32_to_cpu(je->m_rate[i]);
+		je->t_rate[i]		= le32_to_cpu(je->t_rate[i]);
+		je->m_iops[i]		= le32_to_cpu(je->m_iops[i]);
+		je->t_iops[i]		= le32_to_cpu(je->t_iops[i]);
 		je->rate[i]	= le32_to_cpu(je->rate[i]);
 		je->iops[i]	= le32_to_cpu(je->iops[i]);
 	}
@@ -703,7 +692,7 @@
 	je->eta_sec		= le64_to_cpu(je->eta_sec);
 }
 
-static void sum_jobs_eta(struct jobs_eta *dst, struct jobs_eta *je)
+void fio_client_sum_jobs_eta(struct jobs_eta *dst, struct jobs_eta *je)
 {
 	int i;
 
@@ -711,12 +700,12 @@
 	dst->nr_ramp		+= je->nr_ramp;
 	dst->nr_pending		+= je->nr_pending;
 	dst->files_open		+= je->files_open;
-	dst->m_rate		+= je->m_rate;
-	dst->t_rate		+= je->t_rate;
-	dst->m_iops		+= je->m_iops;
-	dst->t_iops		+= je->t_iops;
 
 	for (i = 0; i < 2; i++) {
+		dst->m_rate[i]	+= je->m_rate[i];
+		dst->t_rate[i]	+= je->t_rate[i];
+		dst->m_iops[i]	+= je->m_iops[i];
+		dst->t_iops[i]	+= je->t_iops[i];
 		dst->rate[i]	+= je->rate[i];
 		dst->iops[i]	+= je->iops[i];
 	}
@@ -727,10 +716,10 @@
 		dst->eta_sec = je->eta_sec;
 }
 
-static void dec_jobs_eta(struct client_eta *eta)
+void fio_client_dec_jobs_eta(struct client_eta *eta, void (*fn)(struct jobs_eta *))
 {
 	if (!--eta->pending) {
-		display_thread_status(&eta->eta);
+		fn(&eta->eta);
 		free(eta);
 	}
 }
@@ -771,9 +760,9 @@
 	client->eta_in_flight = NULL;
 	flist_del_init(&client->eta_list);
 
-	convert_jobs_eta(je);
-	sum_jobs_eta(&eta->eta, je);
-	dec_jobs_eta(eta);
+	fio_client_convert_jobs_eta(je);
+	fio_client_sum_jobs_eta(&eta->eta, je);
+	fio_client_dec_jobs_eta(eta, display_thread_status);
 }
 
 static void handle_probe(struct fio_client *client, struct fio_net_cmd *cmd)
@@ -819,7 +808,7 @@
 		log_info("client <%s>: exited with error %d\n", client->hostname, client->error);
 }
 
-static int handle_client(struct fio_client *client, struct client_ops *ops)
+int fio_handle_client(struct fio_client *client, struct client_ops *ops)
 {
 	struct fio_net_cmd *cmd;
 
@@ -917,7 +906,7 @@
 	}
 
 	while (skipped--)
-		dec_jobs_eta(eta);
+		fio_client_dec_jobs_eta(eta, display_thread_status);
 
 	dprint(FD_NET, "client: requested eta tag %p\n", eta);
 }
@@ -984,9 +973,6 @@
 	init_thread_stat(&client_ts);
 	init_group_run_stat(&client_gs);
 
-	/* Used by eta.c:display_thread_status() */
-	update_thread_status = ops->thread_status_display;
-
 	while (!exit_backend && nr_clients) {
 		struct flist_head *entry, *tmp;
 		struct fio_client *client;
@@ -1042,7 +1028,7 @@
 				log_err("fio: unknown client fd %d\n", pfds[i].fd);
 				continue;
 			}
-			if (!handle_client(client, ops)) {
+			if (!fio_handle_client(client, ops)) {
 				log_info("client: host=%s disconnected\n",
 						client->hostname);
 				remove_client(client);
diff --git a/client.h b/client.h
index acf31ff..4146607 100644
--- a/client.h
+++ b/client.h
@@ -6,6 +6,8 @@
 #include <netinet/in.h>
 #include <arpa/inet.h>
 
+#include "stat.h"
+
 struct fio_net_cmd;
 
 struct fio_client {
@@ -64,10 +66,19 @@
 	client_group_stats_op group_stats;
 	client_eta_op eta;
 	client_probe_op probe;
-	client_thread_status_display_op thread_status_display;
 };
 
 extern struct client_ops fio_client_ops;
 
+struct client_eta {
+	struct jobs_eta eta;
+	unsigned int pending;
+};
+
+extern int fio_handle_client(struct fio_client *, struct client_ops *ops);
+extern void fio_client_dec_jobs_eta(struct client_eta *eta, void (*fn)(struct jobs_eta *));
+extern void fio_client_sum_jobs_eta(struct jobs_eta *dst, struct jobs_eta *je);
+extern void fio_client_convert_jobs_eta(struct jobs_eta *je);
+
 #endif
 
diff --git a/eta.c b/eta.c
index 64c02fe..b07ae80 100644
--- a/eta.c
+++ b/eta.c
@@ -7,8 +7,6 @@
 
 #include "fio.h"
 
-void (*update_thread_status)(char *status_message, double perc) = NULL;
-
 static char run_str[REAL_MAX_JOBS + 1];
 
 /*
@@ -85,7 +83,7 @@
 /*
  * Convert seconds to a printable string.
  */
-static void eta_to_str(char *str, unsigned long eta_sec)
+void eta_to_str(char *str, unsigned long eta_sec)
 {
 	unsigned int d, h, m, s;
 	int disp_hour = 0;
@@ -275,11 +273,14 @@
 		    || td->runstate == TD_FSYNCING
 		    || td->runstate == TD_PRE_READING) {
 			je->nr_running++;
-			je->t_rate += td->o.rate[0] + td->o.rate[1];
-			je->m_rate += td->o.ratemin[0] + td->o.ratemin[1];
-			je->t_iops += td->o.rate_iops[0] + td->o.rate_iops[1];
-			je->m_iops += td->o.rate_iops_min[0] +
-					td->o.rate_iops_min[1];
+			je->t_rate[0] += td->o.rate[0];
+			je->t_rate[1] += td->o.rate[1];
+			je->m_rate[0] += td->o.ratemin[0];
+			je->m_rate[1] += td->o.ratemin[1];
+			je->t_iops[0] += td->o.rate_iops[0];
+			je->t_iops[1] += td->o.rate_iops[1];
+			je->m_iops[0] += td->o.rate_iops_min[0];
+			je->m_iops[1] += td->o.rate_iops_min[1];
 			je->files_open += td->nr_open_files;
 		} else if (td->runstate == TD_RAMP) {
 			je->nr_running++;
@@ -368,16 +369,19 @@
 	}
 
 	p += sprintf(p, "Jobs: %d (f=%d)", je->nr_running, je->files_open);
-	if (je->m_rate || je->t_rate) {
+	if (je->m_rate[0] || je->m_rate[1] || je->t_rate[0] || je->t_rate[1]) {
 		char *tr, *mr;
 
-		mr = num2str(je->m_rate, 4, 0, i2p);
-		tr = num2str(je->t_rate, 4, 0, i2p);
+		mr = num2str(je->m_rate[0] + je->m_rate[1], 4, 0, i2p);
+		tr = num2str(je->t_rate[0] + je->t_rate[1], 4, 0, i2p);
 		p += sprintf(p, ", CR=%s/%s KB/s", tr, mr);
 		free(tr);
 		free(mr);
-	} else if (je->m_iops || je->t_iops)
-		p += sprintf(p, ", CR=%d/%d IOPS", je->t_iops, je->m_iops);
+	} else if (je->m_iops[0] || je->m_iops[1] || je->t_iops[0] || je->t_iops[1]) {
+		p += sprintf(p, ", CR=%d/%d IOPS",
+					je->t_iops[0] + je->t_iops[1],
+					je->m_iops[0] + je->t_iops[1]);
+	}
 	if (je->eta_sec != INT_MAX && je->nr_running) {
 		char perc_str[32];
 		char *iops_str[2];
@@ -413,12 +417,8 @@
 	}
 	p += sprintf(p, "\r");
 
-	if (update_thread_status) {
-		update_thread_status(output, perc);
-	} else {
-		printf("%s", output);
-		fflush(stdout);
-	}
+	printf("%s", output);
+	fflush(stdout);
 }
 
 void print_thread_status(void)
diff --git a/gfio.c b/gfio.c
index 382b54b..b14eed3 100644
--- a/gfio.c
+++ b/gfio.c
@@ -28,22 +28,27 @@
 
 #include "fio.h"
 
+static void gfio_update_thread_status(char *status_message, double perc);
+
 #define ARRAYSIZE(x) (sizeof((x)) / (sizeof((x)[0])))
 
 typedef void (*clickfunction)(GtkWidget *widget, gpointer data);
 
-static void quit_clicked(GtkWidget *widget, gpointer data);
+static void connect_clicked(GtkWidget *widget, gpointer data);
 static void start_job_clicked(GtkWidget *widget, gpointer data);
 
 static struct button_spec {
 	const char *buttontext;
 	clickfunction f;
 	const char *tooltiptext;
+	const int start_insensitive;
 } buttonspeclist[] = {
-#define START_JOB_BUTTON 0
+#define CONNECT_BUTTON 0
+#define START_JOB_BUTTON 1
+	{ "Connect", connect_clicked, "Connect to host", 0 },
 	{ "Start Job",
 		start_job_clicked,
-		"Send current fio job to fio server to be executed" },
+		"Send current fio job to fio server to be executed", 1 },
 };
 
 struct probe_widget {
@@ -53,6 +58,19 @@
 	GtkWidget *fio_ver;
 };
 
+struct eta_widget {
+	GtkWidget *jobs;
+	GtkWidget *files;
+	GtkWidget *read_bw;
+	GtkWidget *read_iops;
+	GtkWidget *cr_bw;
+	GtkWidget *cr_iops;
+	GtkWidget *write_bw;
+	GtkWidget *write_iops;
+	GtkWidget *cw_bw;
+	GtkWidget *cw_iops;
+};
+
 struct gui {
 	GtkWidget *window;
 	GtkWidget *vbox;
@@ -74,6 +92,7 @@
 	GtkWidget *error_label;
 	GtkTextBuffer *text;
 	struct probe_widget probe;
+	struct eta_widget eta;
 	pthread_t t;
 
 	void *cookie;
@@ -114,9 +133,97 @@
 	fio_client_ops.group_stats(cmd);
 }
 
+static void gfio_update_eta(struct jobs_eta *je)
+{
+	static int eta_good;
+	char eta_str[128];
+	char output[256];
+	char tmp[32];
+	double perc = 0.0;
+	int i2p = 0;
+
+	eta_str[0] = '\0';
+	output[0] = '\0';
+
+	if (je->eta_sec != INT_MAX && je->elapsed_sec) {
+		perc = (double) je->elapsed_sec / (double) (je->elapsed_sec + je->eta_sec);
+		eta_to_str(eta_str, je->eta_sec);
+	}
+
+	sprintf(tmp, "%u", je->nr_running);
+	gtk_label_set_text(GTK_LABEL(ui.eta.jobs), tmp);
+	sprintf(tmp, "%u", je->files_open);
+	gtk_label_set_text(GTK_LABEL(ui.eta.files), tmp);
+
+#if 0
+	if (je->m_rate[0] || je->m_rate[1] || je->t_rate[0] || je->t_rate[1]) {
+	if (je->m_rate || je->t_rate) {
+		char *tr, *mr;
+
+		mr = num2str(je->m_rate, 4, 0, i2p);
+		tr = num2str(je->t_rate, 4, 0, i2p);
+		gtk_label_set_text(GTK_LABEL(ui.eta.
+		p += sprintf(p, ", CR=%s/%s KB/s", tr, mr);
+		free(tr);
+		free(mr);
+	} else if (je->m_iops || je->t_iops)
+		p += sprintf(p, ", CR=%d/%d IOPS", je->t_iops, je->m_iops);
+#else
+	gtk_label_set_text(GTK_LABEL(ui.eta.cr_bw), "---");
+	gtk_label_set_text(GTK_LABEL(ui.eta.cr_iops), "---");
+	gtk_label_set_text(GTK_LABEL(ui.eta.cw_bw), "---");
+	gtk_label_set_text(GTK_LABEL(ui.eta.cw_iops), "---");
+#endif
+
+	if (je->eta_sec != INT_MAX && je->nr_running) {
+		char *iops_str[2];
+		char *rate_str[2];
+
+		if ((!je->eta_sec && !eta_good) || je->nr_ramp == je->nr_running)
+			strcpy(output, "-.-% done");
+		else {
+			eta_good = 1;
+			perc *= 100.0;
+			sprintf(output, "%3.1f%% done", perc);
+		}
+
+		rate_str[0] = num2str(je->rate[0], 5, 10, i2p);
+		rate_str[1] = num2str(je->rate[1], 5, 10, i2p);
+
+		iops_str[0] = num2str(je->iops[0], 4, 1, 0);
+		iops_str[1] = num2str(je->iops[1], 4, 1, 0);
+
+		gtk_label_set_text(GTK_LABEL(ui.eta.read_bw), rate_str[0]);
+		gtk_label_set_text(GTK_LABEL(ui.eta.read_iops), iops_str[0]);
+		gtk_label_set_text(GTK_LABEL(ui.eta.write_bw), rate_str[1]);
+		gtk_label_set_text(GTK_LABEL(ui.eta.write_iops), iops_str[1]);
+
+		free(rate_str[0]);
+		free(rate_str[1]);
+		free(iops_str[0]);
+		free(iops_str[1]);
+	}
+
+	if (eta_str[0]) {
+		char *dst = output + strlen(output);
+
+		sprintf(dst, " - %s", eta_str);
+	}
+		
+	gfio_update_thread_status(output, perc);
+}
+
 static void gfio_eta_op(struct fio_client *client, struct fio_net_cmd *cmd)
 {
-	fio_client_ops.eta(client, cmd);
+	struct jobs_eta *je = (struct jobs_eta *) cmd->payload;
+	struct client_eta *eta = (struct client_eta *) (uintptr_t) cmd->tag;
+
+	client->eta_in_flight = NULL;
+	flist_del_init(&client->eta_list);
+
+	fio_client_convert_jobs_eta(je);
+	fio_client_sum_jobs_eta(&eta->eta, je);
+	fio_client_dec_jobs_eta(eta, gfio_update_eta);
 }
 
 static void gfio_probe_op(struct fio_client *client, struct fio_net_cmd *cmd)
@@ -165,7 +272,6 @@
 	.group_stats		= gfio_group_stats_op,
 	.eta			= gfio_eta_op,
 	.probe			= gfio_probe_op,
-	.thread_status_display	= gfio_update_thread_status,
 };
 
 static void quit_clicked(__attribute__((unused)) GtkWidget *widget,
@@ -200,8 +306,6 @@
 
 static void start_job_thread(pthread_t *t, struct gui *ui)
 {
-	fio_clients_connect();
-
 	if (send_job_files(ui)) {
 		printf("Yeah, I didn't really like those options too much.\n");
 		gtk_widget_set_sensitive(ui->button[START_JOB_BUTTON], 1);
@@ -216,11 +320,17 @@
 {
 	struct gui *ui = data;
 
-	printf("Start job button was clicked.\n");
 	gtk_widget_set_sensitive(ui->button[START_JOB_BUTTON], 0);
 	start_job_thread(&ui->t, ui);
 }
 
+static void connect_clicked(__attribute__((unused)) GtkWidget *widget,
+                gpointer data)
+{
+	fio_clients_connect();
+	gtk_widget_set_sensitive(ui.button[START_JOB_BUTTON], 1);
+}
+
 static void add_button(struct gui *ui, int i, GtkWidget *buttonbox,
 			struct button_spec *buttonspec)
 {
@@ -228,6 +338,7 @@
 	g_signal_connect(ui->button[i], "clicked", G_CALLBACK (buttonspec->f), ui);
 	gtk_box_pack_start(GTK_BOX (ui->buttonbox), ui->button[i], TRUE, TRUE, 0);
 	gtk_widget_set_tooltip_text(ui->button[i], buttonspeclist[i].tooltiptext);
+	gtk_widget_set_sensitive(ui->button[i], !buttonspec->start_insensitive);
 }
 
 static void add_buttons(struct gui *ui,
@@ -370,7 +481,7 @@
         { "Quit",           GTK_STOCK_QUIT, NULL,   "<Control>Q", NULL, G_CALLBACK(quit_clicked) },
 	{ "About",          GTK_STOCK_ABOUT, NULL,  NULL, NULL, G_CALLBACK(about_dialog) },
 };
-static gint nmenu_items = sizeof (menu_items) / sizeof(menu_items[0]);
+static gint nmenu_items = sizeof(menu_items) / sizeof(menu_items[0]);
 
 static const gchar *ui_string = " \
 	<ui> \
@@ -498,28 +609,44 @@
 	gtk_container_add(GTK_CONTAINER (ui->hostname_hbox), ui->hostname_combo_box);
 	gtk_container_add(GTK_CONTAINER (ui->topvbox), ui->hostname_hbox);
 
-	probe = gtk_frame_new("Host");
+	probe = gtk_frame_new("Job");
 	gtk_box_pack_start(GTK_BOX(ui->topvbox), probe, TRUE, FALSE, 3);
 	probe_frame = gtk_vbox_new(FALSE, 3);
 	gtk_container_add(GTK_CONTAINER(probe), probe_frame);
 
 	probe_box = gtk_hbox_new(FALSE, 3);
 	gtk_box_pack_start(GTK_BOX(probe_frame), probe_box, TRUE, FALSE, 3);
-
 	ui->probe.hostname = new_info_label_in_frame(probe_box, "Host");
 	ui->probe.os = new_info_label_in_frame(probe_box, "OS");
 	ui->probe.arch = new_info_label_in_frame(probe_box, "Architecture");
 	ui->probe.fio_ver = new_info_label_in_frame(probe_box, "Fio version");
 
+	probe_box = gtk_hbox_new(FALSE, 3);
+	gtk_box_pack_start(GTK_BOX(probe_frame), probe_box, TRUE, FALSE, 3);
+	ui->eta.jobs = new_info_label_in_frame(probe_box, "Jobs");
+	ui->eta.files = new_info_label_in_frame(probe_box, "Open files");
+
+	probe_box = gtk_hbox_new(FALSE, 3);
+	gtk_box_pack_start(GTK_BOX(probe_frame), probe_box, TRUE, FALSE, 3);
+	ui->eta.read_bw = new_info_label_in_frame(probe_box, "Read BW");
+	ui->eta.read_iops = new_info_label_in_frame(probe_box, "IOPS");
+	ui->eta.cr_bw = new_info_label_in_frame(probe_box, "Commit BW");
+	ui->eta.cr_iops = new_info_label_in_frame(probe_box, "Commit IOPS");
+
+	probe_box = gtk_hbox_new(FALSE, 3);
+	gtk_box_pack_start(GTK_BOX(probe_frame), probe_box, TRUE, FALSE, 3);
+	ui->eta.write_bw = new_info_label_in_frame(probe_box, "Write BW");
+	ui->eta.write_iops = new_info_label_in_frame(probe_box, "IOPS");
+	ui->eta.cw_bw = new_info_label_in_frame(probe_box, "Commit BW");
+	ui->eta.cw_iops = new_info_label_in_frame(probe_box, "Commit IOPS");
+
 	/*
 	 * Set up thread status progress bar
 	 */
 	ui->thread_status_pb = gtk_progress_bar_new();
-	gtk_progress_bar_set_fraction(
-		GTK_PROGRESS_BAR(ui->thread_status_pb), 0.0);
-	gtk_progress_bar_set_text(
-		GTK_PROGRESS_BAR(ui->thread_status_pb), "No jobs running");
-	gtk_container_add(GTK_CONTAINER (ui->topvbox), ui->thread_status_pb);
+	gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ui->thread_status_pb), 0.0);
+	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ui->thread_status_pb), "No jobs running");
+	gtk_container_add(GTK_CONTAINER(ui->topvbox), ui->thread_status_pb);
 
 	/*
 	 * Add a text box for text op messages 
diff --git a/server.c b/server.c
index f263a0a..2d9b009 100644
--- a/server.c
+++ b/server.c
@@ -443,12 +443,12 @@
 	je->nr_ramp		= cpu_to_le32(je->nr_ramp);
 	je->nr_pending		= cpu_to_le32(je->nr_pending);
 	je->files_open		= cpu_to_le32(je->files_open);
-	je->m_rate		= cpu_to_le32(je->m_rate);
-	je->t_rate		= cpu_to_le32(je->t_rate);
-	je->m_iops		= cpu_to_le32(je->m_iops);
-	je->t_iops		= cpu_to_le32(je->t_iops);
 
 	for (i = 0; i < 2; i++) {
+		je->m_rate[i]	= cpu_to_le32(je->m_rate[i]);
+		je->t_rate[i]	= cpu_to_le32(je->t_rate[i]);
+		je->m_iops[i]	= cpu_to_le32(je->m_iops[i]);
+		je->t_iops[i]	= cpu_to_le32(je->t_iops[i]);
 		je->rate[i]	= cpu_to_le32(je->rate[i]);
 		je->iops[i]	= cpu_to_le32(je->iops[i]);
 	}
diff --git a/server.h b/server.h
index 2be6173..f2dd29f 100644
--- a/server.h
+++ b/server.h
@@ -38,7 +38,7 @@
 };
 
 enum {
-	FIO_SERVER_VER		= 6,
+	FIO_SERVER_VER		= 7,
 
 	FIO_SERVER_MAX_PDU	= 1024,
 
diff --git a/stat.h b/stat.h
index 3115539..bdb858e 100644
--- a/stat.h
+++ b/stat.h
@@ -174,8 +174,8 @@
 	uint32_t nr_ramp;
 	uint32_t nr_pending;
 	uint32_t files_open;
-	uint32_t m_rate, t_rate;
-	uint32_t m_iops, t_iops;
+	uint32_t m_rate[2], t_rate[2];
+	uint32_t m_iops[2], t_iops[2];
 	uint32_t rate[2];
 	uint32_t iops[2];
 	uint64_t elapsed_sec;
@@ -197,5 +197,6 @@
 extern void sum_group_stats(struct group_run_stats *dst, struct group_run_stats *src);
 extern void init_thread_stat(struct thread_stat *ts);
 extern void init_group_run_stat(struct group_run_stats *gs);
+extern void eta_to_str(char *str, unsigned long eta_sec);
 
 #endif