// SPDX-License-Identifier: GPL-2.0-only
/*
 * intel_tpebs.c: Intel TPEBS support
 */

#include <api/fs/fs.h>
#include <sys/param.h>
#include <subcmd/run-command.h>
#include <thread.h>
#include "intel-tpebs.h"
#include <linux/list.h>
#include <linux/zalloc.h>
#include <linux/err.h>
#include "sample.h"
#include "counts.h"
#include "debug.h"
#include "evlist.h"
#include "evsel.h"
#include "mutex.h"
#include "session.h"
#include "stat.h"
#include "tool.h"
#include "cpumap.h"
#include "metricgroup.h"
#include "stat.h"
#include <sys/stat.h>
#include <sys/file.h>
#include <errno.h>
#include <poll.h>
#include <math.h>

#define PERF_DATA		"-"

bool tpebs_recording;
enum tpebs_mode tpebs_mode;
static LIST_HEAD(tpebs_results);
static pthread_t tpebs_reader_thread;
static struct child_process tpebs_cmd;
static int control_fd[2], ack_fd[2];
static struct mutex tpebs_mtx;

struct tpebs_retire_lat {
	struct list_head nd;
	/** @evsel: The evsel that opened the retire_lat event. */
	struct evsel *evsel;
	/** @event: Event passed to perf record. */
	char *event;
	/** @stats: Recorded retirement latency stats. */
	struct stats stats;
	/** @last: Last retirement latency read. */
	uint64_t last;
	/* Has the event been sent to perf record? */
	bool started;
};

static void tpebs_mtx_init(void)
{
	mutex_init(&tpebs_mtx);
}

static struct mutex *tpebs_mtx_get(void)
{
	static pthread_once_t tpebs_mtx_once = PTHREAD_ONCE_INIT;

	pthread_once(&tpebs_mtx_once, tpebs_mtx_init);
	return &tpebs_mtx;
}

static struct tpebs_retire_lat *tpebs_retire_lat__find(struct evsel *evsel)
	EXCLUSIVE_LOCKS_REQUIRED(tpebs_mtx_get());

static int evsel__tpebs_start_perf_record(struct evsel *evsel)
{
	const char **record_argv;
	int tpebs_event_size = 0, i = 0, ret;
	char control_fd_buf[32];
	char cpumap_buf[50];
	struct tpebs_retire_lat *t;

	list_for_each_entry(t, &tpebs_results, nd)
		tpebs_event_size++;

	record_argv = malloc((10 + 2 * tpebs_event_size) * sizeof(*record_argv));
	if (!record_argv)
		return -ENOMEM;

	record_argv[i++] = "perf";
	record_argv[i++] = "record";
	record_argv[i++] = "-W";
	record_argv[i++] = "--synth=no";

	scnprintf(control_fd_buf, sizeof(control_fd_buf), "--control=fd:%d,%d",
		  control_fd[0], ack_fd[1]);
	record_argv[i++] = control_fd_buf;

	record_argv[i++] = "-o";
	record_argv[i++] = PERF_DATA;

	if (!perf_cpu_map__is_any_cpu_or_is_empty(evsel->evlist->core.user_requested_cpus)) {
		cpu_map__snprint(evsel->evlist->core.user_requested_cpus, cpumap_buf,
				 sizeof(cpumap_buf));
		record_argv[i++] = "-C";
		record_argv[i++] = cpumap_buf;
	}

	list_for_each_entry(t, &tpebs_results, nd) {
		record_argv[i++] = "-e";
		record_argv[i++] = t->event;
	}
	record_argv[i++] = NULL;
	assert(i == 10 + 2 * tpebs_event_size || i == 8 + 2 * tpebs_event_size);
	/* Note, no workload given so system wide is implied. */

	assert(tpebs_cmd.pid == 0);
	tpebs_cmd.argv = record_argv;
	tpebs_cmd.out = -1;
	ret = start_command(&tpebs_cmd);
	zfree(&tpebs_cmd.argv);
	list_for_each_entry(t, &tpebs_results, nd)
		t->started = true;

	return ret;
}

static bool is_child_pid(pid_t parent, pid_t child)
{
	if (parent < 0 || child < 0)
		return false;

	while (true) {
		char path[PATH_MAX];
		char line[256];
		FILE *fp;

new_child:
		if (parent == child)
			return true;

		if (child <= 0)
			return false;

		scnprintf(path, sizeof(path), "%s/%d/status", procfs__mountpoint(), child);
		fp = fopen(path, "r");
		if (!fp) {
			/* Presumably the process went away. Assume not a child. */
			return false;
		}
		while (fgets(line, sizeof(line), fp) != NULL) {
			if (strncmp(line, "PPid:", 5) == 0) {
				fclose(fp);
				if (sscanf(line + 5, "%d", &child) != 1) {
					/* Unexpected error parsing. */
					return false;
				}
				goto new_child;
			}
		}
		/* Unexpected EOF. */
		fclose(fp);
		return false;
	}
}

static bool should_ignore_sample(const struct perf_sample *sample, const struct tpebs_retire_lat *t)
{
	pid_t workload_pid, sample_pid = sample->pid;

	/*
	 * During evlist__purge the evlist will be removed prior to the
	 * evsel__exit calling evsel__tpebs_close and taking the
	 * tpebs_mtx. Avoid a segfault by ignoring samples in this case.
	 */
	if (t->evsel->evlist == NULL)
		return true;

	workload_pid = t->evsel->evlist->workload.pid;
	if (workload_pid < 0 || workload_pid == sample_pid)
		return false;

	if (!t->evsel->core.attr.inherit)
		return true;

	return !is_child_pid(workload_pid, sample_pid);
}

static int process_sample_event(const struct perf_tool *tool __maybe_unused,
				union perf_event *event __maybe_unused,
				struct perf_sample *sample,
				struct evsel *evsel,
				struct machine *machine __maybe_unused)
{
	struct tpebs_retire_lat *t;

	mutex_lock(tpebs_mtx_get());
	if (tpebs_cmd.pid == 0) {
		/* Record has terminated. */
		mutex_unlock(tpebs_mtx_get());
		return 0;
	}
	t = tpebs_retire_lat__find(evsel);
	if (!t) {
		mutex_unlock(tpebs_mtx_get());
		return -EINVAL;
	}
	if (should_ignore_sample(sample, t)) {
		mutex_unlock(tpebs_mtx_get());
		return 0;
	}
	/*
	 * Need to handle per core results? We are assuming average retire
	 * latency value will be used. Save the number of samples and the sum of
	 * retire latency value for each event.
	 */
	t->last = sample->weight3;
	update_stats(&t->stats, sample->weight3);
	mutex_unlock(tpebs_mtx_get());
	return 0;
}

static int process_feature_event(const struct perf_tool *tool __maybe_unused,
				 struct perf_session *session,
				 union perf_event *event)
{
	if (event->feat.feat_id < HEADER_LAST_FEATURE)
		return perf_event__process_feature(session, event);
	return 0;
}

static void *__sample_reader(void *arg __maybe_unused)
{
	struct perf_session *session;
	struct perf_data data = {
		.mode = PERF_DATA_MODE_READ,
		.path = PERF_DATA,
		.file.fd = tpebs_cmd.out,
	};
	struct perf_tool tool;

	perf_tool__init(&tool, /*ordered_events=*/false);
	tool.sample = process_sample_event;
	tool.feature = process_feature_event;
	tool.attr = perf_event__process_attr;

	session = perf_session__new(&data, &tool);
	if (IS_ERR(session))
		return NULL;
	perf_session__process_events(session);
	perf_session__delete(session);

	return NULL;
}

static int tpebs_send_record_cmd(const char *msg) EXCLUSIVE_LOCKS_REQUIRED(tpebs_mtx_get())
{
	struct pollfd pollfd = { .events = POLLIN, };
	int ret, len, retries = 0;
	char ack_buf[8];

	/* Check if the command exited before the send, done with the lock held. */
	if (tpebs_cmd.pid == 0)
		return 0;

	/*
	 * Let go of the lock while sending/receiving as blocking can starve the
	 * sample reading thread.
	 */
	mutex_unlock(tpebs_mtx_get());

	/* Send perf record command.*/
	len = strlen(msg);
	ret = write(control_fd[1], msg, len);
	if (ret != len) {
		pr_err("perf record control write control message '%s' failed\n", msg);
		ret = -EPIPE;
		goto out;
	}

	if (!strcmp(msg, EVLIST_CTL_CMD_STOP_TAG)) {
		ret = 0;
		goto out;
	}

	/* Wait for an ack. */
	pollfd.fd = ack_fd[0];

	/*
	 * We need this poll to ensure the ack_fd PIPE will not hang
	 * when perf record failed for any reason. The timeout value
	 * 3000ms is an empirical selection.
	 */
again:
	if (!poll(&pollfd, 1, 500)) {
		if (check_if_command_finished(&tpebs_cmd)) {
			ret = 0;
			goto out;
		}

		if (retries++ < 6)
			goto again;
		pr_err("tpebs failed: perf record ack timeout for '%s'\n", msg);
		ret = -ETIMEDOUT;
		goto out;
	}

	if (!(pollfd.revents & POLLIN)) {
		if (check_if_command_finished(&tpebs_cmd)) {
			ret = 0;
			goto out;
		}

		pr_err("tpebs failed: did not received an ack for '%s'\n", msg);
		ret = -EPIPE;
		goto out;
	}

	ret = read(ack_fd[0], ack_buf, sizeof(ack_buf));
	if (ret > 0)
		ret = strcmp(ack_buf, EVLIST_CTL_CMD_ACK_TAG);
	else
		pr_err("tpebs: perf record control ack failed\n");
out:
	/* Re-take lock as expected by caller. */
	mutex_lock(tpebs_mtx_get());
	return ret;
}

/*
 * tpebs_stop - stop the sample data read thread and the perf record process.
 */
static int tpebs_stop(void) EXCLUSIVE_LOCKS_REQUIRED(tpebs_mtx_get())
{
	int ret = 0;

	/* Like tpebs_start, we should only run tpebs_end once. */
	if (tpebs_cmd.pid != 0) {
		tpebs_send_record_cmd(EVLIST_CTL_CMD_STOP_TAG);
		tpebs_cmd.pid = 0;
		mutex_unlock(tpebs_mtx_get());
		pthread_join(tpebs_reader_thread, NULL);
		mutex_lock(tpebs_mtx_get());
		close(control_fd[0]);
		close(control_fd[1]);
		close(ack_fd[0]);
		close(ack_fd[1]);
		close(tpebs_cmd.out);
		ret = finish_command(&tpebs_cmd);
		tpebs_cmd.pid = 0;
		if (ret == -ERR_RUN_COMMAND_WAITPID_SIGNAL)
			ret = 0;
	}
	return ret;
}

/**
 * evsel__tpebs_event() - Create string event encoding to pass to `perf record`.
 */
static int evsel__tpebs_event(struct evsel *evsel, char **event)
{
	char *name, *modifier;
	int ret;

	name = strdup(evsel->name);
	if (!name)
		return -ENOMEM;

	modifier = strrchr(name, 'R');
	if (!modifier) {
		ret = -EINVAL;
		goto out;
	}
	*modifier = 'p';
	modifier = strchr(name, ':');
	if (!modifier)
		modifier = strrchr(name, '/');
	if (!modifier) {
		ret = -EINVAL;
		goto out;
	}
	*modifier = '\0';
	if (asprintf(event, "%s/name=tpebs_event_%p/%s", name, evsel, modifier + 1) > 0)
		ret = 0;
	else
		ret = -ENOMEM;
out:
	if (ret)
		pr_err("Tpebs event modifier broken '%s'\n", evsel->name);
	free(name);
	return ret;
}

static struct tpebs_retire_lat *tpebs_retire_lat__new(struct evsel *evsel)
{
	struct tpebs_retire_lat *result = zalloc(sizeof(*result));
	int ret;

	if (!result)
		return NULL;

	ret = evsel__tpebs_event(evsel, &result->event);
	if (ret) {
		free(result);
		return NULL;
	}
	result->evsel = evsel;
	return result;
}

static void tpebs_retire_lat__delete(struct tpebs_retire_lat *r)
{
	zfree(&r->event);
	free(r);
}

static struct tpebs_retire_lat *tpebs_retire_lat__find(struct evsel *evsel)
{
	struct tpebs_retire_lat *t;
	unsigned long num;
	const char *evsel_name;

	/*
	 * Evsels will match for evlist with the retirement latency event. The
	 * name with "tpebs_event_" prefix will be present on events being read
	 * from `perf record`.
	 */
	if (evsel__is_retire_lat(evsel)) {
		list_for_each_entry(t, &tpebs_results, nd) {
			if (t->evsel == evsel)
				return t;
		}
		return NULL;
	}
	evsel_name = strstr(evsel->name, "tpebs_event_");
	if (!evsel_name) {
		/* Unexpected that the perf record should have other events. */
		return NULL;
	}
	errno = 0;
	num = strtoull(evsel_name + 12, NULL, 16);
	if (errno) {
		pr_err("Bad evsel for tpebs find '%s'\n", evsel->name);
		return NULL;
	}
	list_for_each_entry(t, &tpebs_results, nd) {
		if ((unsigned long)t->evsel == num)
			return t;
	}
	return NULL;
}

/**
 * evsel__tpebs_prepare - create tpebs data structures ready for opening.
 * @evsel: retire_latency evsel, all evsels on its list will be prepared.
 */
static int evsel__tpebs_prepare(struct evsel *evsel)
{
	struct evsel *pos;
	struct tpebs_retire_lat *tpebs_event;

	mutex_lock(tpebs_mtx_get());
	tpebs_event = tpebs_retire_lat__find(evsel);
	if (tpebs_event) {
		/* evsel, or an identically named one, was already prepared. */
		mutex_unlock(tpebs_mtx_get());
		return 0;
	}
	tpebs_event = tpebs_retire_lat__new(evsel);
	if (!tpebs_event) {
		mutex_unlock(tpebs_mtx_get());
		return -ENOMEM;
	}
	list_add_tail(&tpebs_event->nd, &tpebs_results);
	mutex_unlock(tpebs_mtx_get());

	/*
	 * Eagerly prepare all other evsels on the list to try to ensure that by
	 * open they are all known.
	 */
	evlist__for_each_entry(evsel->evlist, pos) {
		int ret;

		if (pos == evsel || !pos->retire_lat)
			continue;

		ret = evsel__tpebs_prepare(pos);
		if (ret)
			return ret;
	}
	return 0;
}

/**
 * evsel__tpebs_open - starts tpebs execution.
 * @evsel: retire_latency evsel, all evsels on its list will be selected. Each
 *         evsel is sampled to get the average retire_latency value.
 */
int evsel__tpebs_open(struct evsel *evsel)
{
	int ret;
	bool tpebs_empty;

	/* We should only run tpebs_start when tpebs_recording is enabled. */
	if (!tpebs_recording)
		return 0;
	/* Only start the events once. */
	if (tpebs_cmd.pid != 0) {
		struct tpebs_retire_lat *t;
		bool valid;

		mutex_lock(tpebs_mtx_get());
		t = tpebs_retire_lat__find(evsel);
		valid = t && t->started;
		mutex_unlock(tpebs_mtx_get());
		/* May fail as the event wasn't started. */
		return valid ? 0 : -EBUSY;
	}

	ret = evsel__tpebs_prepare(evsel);
	if (ret)
		return ret;

	mutex_lock(tpebs_mtx_get());
	tpebs_empty = list_empty(&tpebs_results);
	if (!tpebs_empty) {
		/*Create control and ack fd for --control*/
		if (pipe(control_fd) < 0) {
			pr_err("tpebs: Failed to create control fifo");
			ret = -1;
			goto out;
		}
		if (pipe(ack_fd) < 0) {
			pr_err("tpebs: Failed to create control fifo");
			ret = -1;
			goto out;
		}

		ret = evsel__tpebs_start_perf_record(evsel);
		if (ret)
			goto out;

		if (pthread_create(&tpebs_reader_thread, /*attr=*/NULL, __sample_reader,
				   /*arg=*/NULL)) {
			kill(tpebs_cmd.pid, SIGTERM);
			close(tpebs_cmd.out);
			pr_err("Could not create thread to process sample data.\n");
			ret = -1;
			goto out;
		}
		ret = tpebs_send_record_cmd(EVLIST_CTL_CMD_ENABLE_TAG);
	}
out:
	if (ret) {
		struct tpebs_retire_lat *t = tpebs_retire_lat__find(evsel);

		list_del_init(&t->nd);
		tpebs_retire_lat__delete(t);
	}
	mutex_unlock(tpebs_mtx_get());
	return ret;
}

int evsel__tpebs_read(struct evsel *evsel, int cpu_map_idx, int thread)
{
	struct perf_counts_values *count, *old_count = NULL;
	struct tpebs_retire_lat *t;
	uint64_t val;
	int ret;

	/* Only set retire_latency value to the first CPU and thread. */
	if (cpu_map_idx != 0 || thread != 0)
		return 0;

	if (evsel->prev_raw_counts)
		old_count = perf_counts(evsel->prev_raw_counts, cpu_map_idx, thread);

	count = perf_counts(evsel->counts, cpu_map_idx, thread);

	mutex_lock(tpebs_mtx_get());
	t = tpebs_retire_lat__find(evsel);
	/*
	 * If reading the first tpebs result, send a ping to the record
	 * process. Allow the sample reader a chance to read by releasing and
	 * reacquiring the lock.
	 */
	if (t && &t->nd == tpebs_results.next) {
		ret = tpebs_send_record_cmd(EVLIST_CTL_CMD_PING_TAG);
		mutex_unlock(tpebs_mtx_get());
		if (ret)
			return ret;
		mutex_lock(tpebs_mtx_get());
	}
	if (t == NULL || t->stats.n == 0) {
		/* No sample data, use default. */
		if (tpebs_recording) {
			pr_warning_once(
				"Using precomputed retirement latency data as no samples\n");
		}
		val = 0;
		switch (tpebs_mode) {
		case TPEBS_MODE__MIN:
			val = rint(evsel->retirement_latency.min);
			break;
		case TPEBS_MODE__MAX:
			val = rint(evsel->retirement_latency.max);
			break;
		default:
		case TPEBS_MODE__LAST:
		case TPEBS_MODE__MEAN:
			val = rint(evsel->retirement_latency.mean);
			break;
		}
	} else {
		switch (tpebs_mode) {
		case TPEBS_MODE__MIN:
			val = t->stats.min;
			break;
		case TPEBS_MODE__MAX:
			val = t->stats.max;
			break;
		case TPEBS_MODE__LAST:
			val = t->last;
			break;
		default:
		case TPEBS_MODE__MEAN:
			val = rint(t->stats.mean);
			break;
		}
	}
	mutex_unlock(tpebs_mtx_get());

	if (old_count) {
		count->val = old_count->val + val;
		count->run = old_count->run + 1;
		count->ena = old_count->ena + 1;
	} else {
		count->val = val;
		count->run++;
		count->ena++;
	}
	return 0;
}

/**
 * evsel__tpebs_close() - delete tpebs related data. If the last event, stop the
 * created thread and process by calling tpebs_stop().
 *
 * This function is called in evsel__close() to be symmetric with
 * evsel__tpebs_open() being called in evsel__open().
 */
void evsel__tpebs_close(struct evsel *evsel)
{
	struct tpebs_retire_lat *t;

	mutex_lock(tpebs_mtx_get());
	t = tpebs_retire_lat__find(evsel);
	if (t) {
		list_del_init(&t->nd);
		tpebs_retire_lat__delete(t);

		if (list_empty(&tpebs_results))
			tpebs_stop();
	}
	mutex_unlock(tpebs_mtx_get());
}
