Commit 3bbec828 authored by Joanne Hugé's avatar Joanne Hugé

Merge branch 'packet-exchange'

parents 147e41be 4c33499c
...@@ -54,9 +54,9 @@ typedef struct network_config { ...@@ -54,9 +54,9 @@ typedef struct network_config {
// Static functions // Static functions
static void process_options(int argc, char *argv[]); static void process_options(int argc, char *argv[]);
static void do_tsn_task(struct thread_param *param, char * data, uint64_t next_txtime); static void do_tsn_task(struct thread_param *param, char *data, uint64_t next_txtime);
static void print_histograms(); static void print_histograms();
static void sigint_handler(int sig_num); static void sighand(int sig_num);
// Static variables // Static variables
...@@ -122,13 +122,12 @@ static void *packet_sending_thread(void *p) { ...@@ -122,13 +122,12 @@ static void *packet_sending_thread(void *p) {
if (sched_setscheduler(0, SCHED_FIFO, &priority)) if (sched_setscheduler(0, SCHED_FIFO, &priority))
error(EXIT_FAILURE, errno, "Couldn't set priority"); error(EXIT_FAILURE, errno, "Couldn't set priority");
if(enable_etf) { if (enable_etf) {
// Measure from CLOCK_TAI to generate timestamp // Measure from CLOCK_TAI to generate timestamp
clock_gettime(CLOCK_TAI, &next); clock_gettime(CLOCK_TAI, &next);
next_txtime = next.tv_sec * NSEC_PER_SEC + next.tv_nsec; next_txtime = next.tv_sec * NSEC_PER_SEC + next.tv_nsec;
next_txtime += param->etf_offset; next_txtime += param->etf_offset;
} } else {
else {
next_txtime = 0; next_txtime = 0;
} }
...@@ -145,7 +144,7 @@ static void *packet_sending_thread(void *p) { ...@@ -145,7 +144,7 @@ static void *packet_sending_thread(void *p) {
add_ns(&next, param->interval); add_ns(&next, param->interval);
if(enable_etf) if (enable_etf)
next_txtime += param->interval; next_txtime += param->interval;
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL); clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
...@@ -160,9 +159,6 @@ int main(int argc, char *argv[]) { ...@@ -160,9 +159,6 @@ int main(int argc, char *argv[]) {
pthread_t thread; pthread_t thread;
thread_stat_t *stats; thread_stat_t *stats;
// Catch breaks with sigint_handler
signal(SIGINT, sigint_handler);
param = malloc(sizeof(thread_param_t)); param = malloc(sizeof(thread_param_t));
stats = &param->stats; stats = &param->stats;
...@@ -191,6 +187,9 @@ int main(int argc, char *argv[]) { ...@@ -191,6 +187,9 @@ int main(int argc, char *argv[]) {
memset((int64_t *)histograms, 0, NB_HISTOGRAMS * MAX_HIST_VAL); memset((int64_t *)histograms, 0, NB_HISTOGRAMS * MAX_HIST_VAL);
} }
// Catch breaks with sighand to print the histograms
init_signals(sighand, enable_histograms);
// Initialize the UDP packet sending socket // Initialize the UDP packet sending socket
init_udp_send(enable_etf, enable_timestamps, init_udp_send(enable_etf, enable_timestamps,
network_config.packet_priority, network_config.packet_priority,
...@@ -240,7 +239,7 @@ int main(int argc, char *argv[]) { ...@@ -240,7 +239,7 @@ int main(int argc, char *argv[]) {
} }
// Critical TSN task // Critical TSN task
static void do_tsn_task(struct thread_param *param, char * data, uint64_t next_txtime) { static void do_tsn_task(struct thread_param *param, char *data, uint64_t next_txtime) {
struct timespec t1, t2; struct timespec t1, t2;
int rtt_us; int rtt_us;
...@@ -262,9 +261,9 @@ static void do_tsn_task(struct thread_param *param, char * data, uint64_t next_t ...@@ -262,9 +261,9 @@ static void do_tsn_task(struct thread_param *param, char * data, uint64_t next_t
param->stats.rtt = calcdiff_ns(t2, t1); param->stats.rtt = calcdiff_ns(t2, t1);
if(enable_histograms) { if (enable_histograms) {
rtt_us = param->stats.rtt / 1000; rtt_us = param->stats.rtt / 1000;
if(rtt_us > MAX_HIST_VAL) { if (rtt_us > MAX_HIST_VAL) {
fprintf(stderr, "RTT value higher than MAX_HIST_VAL : %d ( > %d)\n", rtt_us, MAX_HIST_VAL); fprintf(stderr, "RTT value higher than MAX_HIST_VAL : %d ( > %d)\n", rtt_us, MAX_HIST_VAL);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
...@@ -288,26 +287,28 @@ static void print_histograms() { ...@@ -288,26 +287,28 @@ static void print_histograms() {
interval = param->interval / 1000; interval = param->interval / 1000;
if(tsn_task == SEND_PACKET_TASK) { if (tsn_task == SEND_PACKET_TASK) {
printf("{\"measure_sets\": [{" printf("{\"measure_sets\": [{"
"\"measure_type\": \"packet_send_timestamps\"," "\"measure_type\": \"packet_send_timestamps\","
"\"props_names\": [\"user_space\", \"kernel_space\"]," "\"props_names\": [\"user_space\", \"kernel_space\"],"
"\"units\": [\"us\", \"us\"]," "\"units\": [\"us\", \"us\"],"
"\"props_type\": \"histogram\"," "\"props_type\": \"histogram\","
"\"metadata\": {" "\"metadata\": {"
"\"i\": \"%dus\", \"duration\": \"%dh%d\"" "\"i\": \"%dus\", \"duration\": \"%dh%d\", \"etf_offset\": \"%dus\","
"}," "},"
"\"props\": [", interval, duration_hour, duration_minutes); "\"props\": [",
} else if(tsn_task == RTT_TASK) { interval, duration_hour, duration_minutes, param->etf_offset);
} else if (tsn_task == RTT_TASK) {
printf("{\"measure_sets\": [{" printf("{\"measure_sets\": [{"
"\"measure_type\": \"packet_rtt\"," "\"measure_type\": \"packet_rtt\","
"\"props_names\": [\"rtt\"]," "\"props_names\": [\"rtt\"],"
"\"units\": [\"us\"]," "\"units\": [\"us\"],"
"\"props_type\": \"histogram\"," "\"props_type\": \"histogram\","
"\"metadata\": {" "\"metadata\": {"
"\"i\": \"%dus\", \"duration\": \"%dh%d\"" "\"i\": \"%dus\", \"duration\": \"%dh%d\", \"etf_offset\": \"%dus\","
"}," "},"
"\"props\": [", interval, duration_hour, duration_minutes); "\"props\": [",
interval, duration_hour, duration_minutes, param->etf_offset);
} }
nb_hists = tsn_task == SEND_PACKET_TASK ? 2 : 1; nb_hists = tsn_task == SEND_PACKET_TASK ? 2 : 1;
...@@ -325,13 +326,12 @@ static void print_histograms() { ...@@ -325,13 +326,12 @@ static void print_histograms() {
printf("%s", (i + 1 < nb_hists ? "], " : "]")); printf("%s", (i + 1 < nb_hists ? "], " : "]"));
} }
printf( "]}]}\n"); printf("]}]}\n");
} }
static void sigint_handler(int sig_num) { static void sighand(int sig_num) {
(void)sig_num; (void)sig_num;
if (enable_histograms) print_histograms();
print_histograms();
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
} }
......
...@@ -107,11 +107,11 @@ packet_info_t recv_udp_packet(int use_timestamps, int use_histograms, int64_t hi ...@@ -107,11 +107,11 @@ packet_info_t recv_udp_packet(int use_timestamps, int use_histograms, int64_t hi
struct cmsghdr *cmsg; struct cmsghdr *cmsg;
struct msghdr msg; // Message hardware, sent to the socket struct msghdr msg; // Message hardware, sent to the socket
struct iovec iov; // The iovec structures stores the RX buffer struct iovec iov; // The iovec structures stores the RX buffer
struct sockaddr_in sin; struct sockaddr_in sin;
struct { struct {
struct cmsghdr cm; struct cmsghdr cm;
char control[512]; char control[512];
} control; } control;
int recvmsgerr; int recvmsgerr;
...@@ -144,7 +144,7 @@ packet_info_t recv_udp_packet(int use_timestamps, int use_histograms, int64_t hi ...@@ -144,7 +144,7 @@ packet_info_t recv_udp_packet(int use_timestamps, int use_histograms, int64_t hi
packet_info.userspace_enter_ts = ts_to_uint(ts); packet_info.userspace_enter_ts = ts_to_uint(ts);
} }
if(use_timestamps) { if (use_timestamps) {
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMPING) { if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMPING) {
...@@ -154,7 +154,7 @@ packet_info_t recv_udp_packet(int use_timestamps, int use_histograms, int64_t hi ...@@ -154,7 +154,7 @@ packet_info_t recv_udp_packet(int use_timestamps, int use_histograms, int64_t hi
clock_gettime(CLOCK_REALTIME, &ts); clock_gettime(CLOCK_REALTIME, &ts);
packet_info.userspace_exit_ts = ts_to_uint(ts); packet_info.userspace_exit_ts = ts_to_uint(ts);
if(use_histograms) if (use_histograms)
fill_histograms(&packet_info, histograms); fill_histograms(&packet_info, histograms);
} }
} }
......
...@@ -42,7 +42,7 @@ static void init_tx_buffer(size_t _tx_buffer_len); ...@@ -42,7 +42,7 @@ static void init_tx_buffer(size_t _tx_buffer_len);
static int so_priority = 3; static int so_priority = 3;
static struct sock_txtime sk_txtime; static struct sock_txtime sk_txtime;
static unsigned char *tx_buffer; static char *tx_buffer;
static size_t tx_buffer_len; static size_t tx_buffer_len;
static int sock_fd; static int sock_fd;
...@@ -128,15 +128,15 @@ void init_udp_send(int use_etf, int use_timestamps, int packet_priority, ...@@ -128,15 +128,15 @@ void init_udp_send(int use_etf, int use_timestamps, int packet_priority,
* Sends udp packets * Sends udp packets
*/ */
packet_info_t send_udp_packet(int use_etf, int use_timestamps, packet_info_t send_udp_packet(int use_etf, int use_timestamps,
char * data, char *data,
uint64_t txtime, uint64_t txtime,
const char *server_ip, const char *server_ip,
int64_t histograms[NB_HISTOGRAMS][MAX_HIST_VAL]) { int64_t histograms[NB_HISTOGRAMS][MAX_HIST_VAL]) {
struct msghdr msg; // Message hardware, sent to the socket struct msghdr msg; // Message hardware, sent to the socket
struct cmsghdr *cmsg; // Control message hardware, for txtime struct cmsghdr *cmsg; // Control message hardware, for txtime
char control[CMSG_SPACE(sizeof(txtime))] = {}; // Stores txtime char control[CMSG_SPACE(sizeof(txtime))] = {}; // Stores txtime
struct iovec iov; // The iovec structures stores the TX buffer struct iovec iov; // The iovec structures stores the TX buffer
// Poll file descriptor, used to poll for timestamp messages // Poll file descriptor, used to poll for timestamp messages
struct pollfd poll_fd = {sock_fd, POLLPRI, 0}; struct pollfd poll_fd = {sock_fd, POLLPRI, 0};
int sendmsgerr, pollerr; int sendmsgerr, pollerr;
...@@ -260,7 +260,6 @@ static void process_timestamps(packet_info_t *packet_info, int64_t histograms[NB ...@@ -260,7 +260,6 @@ static void process_timestamps(packet_info_t *packet_info, int64_t histograms[NB
fprintf(stderr, "process_timestamps: level %d type %d", cmsg->cmsg_level, fprintf(stderr, "process_timestamps: level %d type %d", cmsg->cmsg_level,
cmsg->cmsg_type); cmsg->cmsg_type);
#endif #endif
} }
} }
} }
......
...@@ -4,6 +4,6 @@ ...@@ -4,6 +4,6 @@
#include "utilities.h" #include "utilities.h"
void init_udp_send(int use_etf, int use_timestamps, int so_priority, char *network_if, size_t tx_buffer_len); void init_udp_send(int use_etf, int use_timestamps, int so_priority, char *network_if, size_t tx_buffer_len);
packet_info_t send_udp_packet(int use_etf, int use_timestamps, char * data, uint64_t txtime, const char *server_ip, int64_t histograms[NB_HISTOGRAMS][MAX_HIST_VAL]); packet_info_t send_udp_packet(int use_etf, int use_timestamps, char *data, uint64_t txtime, const char *server_ip, int64_t histograms[NB_HISTOGRAMS][MAX_HIST_VAL]);
#endif #endif
...@@ -60,7 +60,7 @@ typedef struct main_param { ...@@ -60,7 +60,7 @@ typedef struct main_param {
static void process_options(int argc, char *argv[]); static void process_options(int argc, char *argv[]);
static void print_histograms(); static void print_histograms();
static void sigint_handler(int sig_num); static void sighand(int sig_num);
// Static variables // Static variables
...@@ -136,7 +136,7 @@ static void *packet_receiving_thread(void *p) { ...@@ -136,7 +136,7 @@ static void *packet_receiving_thread(void *p) {
} else if (tsn_task == RECV_PACKET_TASK) { } else if (tsn_task == RECV_PACKET_TASK) {
int current_packet_id; int current_packet_id;
param->stats.packet_info = recv_udp_packet(enable_timestamps, enable_histograms, histograms); param->stats.packet_info = recv_udp_packet(enable_timestamps, enable_histograms, histograms);
...@@ -154,14 +154,13 @@ static void *packet_receiving_thread(void *p) { ...@@ -154,14 +154,13 @@ static void *packet_receiving_thread(void *p) {
// Check if packets were lost // Check if packets were lost
param->stats.lost_packets += (current_packet_id - prev_packet_id - 1) % 1000; param->stats.lost_packets += (current_packet_id - prev_packet_id - 1) % 1000;
if(enable_histograms) { if (enable_histograms) {
dist_to_interval = (((int64_t)diff) - param->interval) / 1000; dist_to_interval = (((int64_t)diff) - param->interval) / 1000;
dist_to_interval += MAX_HIST_VAL / 2; dist_to_interval += MAX_HIST_VAL / 2;
if( dist_to_interval > ((int)MAX_HIST_VAL) || dist_to_interval < 0 ) { if (dist_to_interval > ((int)MAX_HIST_VAL) || dist_to_interval < 0) {
fprintf(stderr, "jitter higher than MAX_HIST_VAL: %" PRIi64 "\n", dist_to_interval); fprintf(stderr, "jitter higher than MAX_HIST_VAL: %" PRIi64 "\n", dist_to_interval);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} } else {
else {
histograms[2][dist_to_interval]++; histograms[2][dist_to_interval]++;
} }
} }
...@@ -182,9 +181,6 @@ int main(int argc, char *argv[]) { ...@@ -182,9 +181,6 @@ int main(int argc, char *argv[]) {
thread_stat_t *stats; thread_stat_t *stats;
int64_t diff; int64_t diff;
// Catch breaks with sigint_handler
signal(SIGINT, sigint_handler);
param = malloc(sizeof(thread_param_t)); param = malloc(sizeof(thread_param_t));
stats = &param->stats; stats = &param->stats;
...@@ -210,6 +206,9 @@ int main(int argc, char *argv[]) { ...@@ -210,6 +206,9 @@ int main(int argc, char *argv[]) {
memset((int64_t *)histograms, 0, NB_HISTOGRAMS * MAX_HIST_VAL); memset((int64_t *)histograms, 0, NB_HISTOGRAMS * MAX_HIST_VAL);
} }
// Catch breaks with sighand to print the histograms
init_signals(sighand, enable_histograms);
// Initialize the UDP packet receiving socket // Initialize the UDP packet receiving socket
init_udp_recv(enable_timestamps, network_config.network_if); init_udp_recv(enable_timestamps, network_config.network_if);
...@@ -225,17 +224,17 @@ int main(int argc, char *argv[]) { ...@@ -225,17 +224,17 @@ int main(int argc, char *argv[]) {
for (;;) { for (;;) {
usleep(main_param.refresh_rate); usleep(main_param.refresh_rate);
if(main_param.verbose) { if (main_param.verbose) {
if(tsn_task == RECV_PACKET_TASK) { if (tsn_task == RECV_PACKET_TASK) {
diff = ((int64_t)stats->max_interval) - stats->min_interval; diff = ((int64_t)stats->max_interval) - stats->min_interval;
printf( "(%d) Jitter : %" PRIi64 " [Packet data: %s] [Lost packets: %d]\n", printf("(%d) Jitter : %" PRIi64 " [Packet data: %s] [Lost packets: %d]\n",
stats->packets_received, stats->packets_received,
diff, diff,
stats->packet_info.data, stats->packet_info.data,
stats->lost_packets); stats->lost_packets);
if(enable_timestamps) { if (enable_timestamps) {
printf("(%d) Enter send_udp_packet timestamp: %" PRIu64 "\n", printf("(%d) Enter send_udp_packet timestamp: %" PRIu64 "\n",
stats->packets_received, stats->packets_received,
stats->packet_info.userspace_enter_ts); stats->packet_info.userspace_enter_ts);
...@@ -266,16 +265,17 @@ static void print_histograms() { ...@@ -266,16 +265,17 @@ static void print_histograms() {
interval = param->interval / 1000; interval = param->interval / 1000;
if(enable_timestamps) { if (enable_timestamps) {
printf("{\"measure_sets\": [{" printf("{\"measure_sets\": [{"
"\"measure_type\": \"packet_recv_timestamps\"," "\"measure_type\": \"packet_recv_timestamps\","
"\"props_names\": [\"user_space\", \"kernel_space\"]," "\"props_names\": [\"user_space\", \"kernel_space\"],"
"\"units\": [\"us\", \"us\"]," "\"units\": [\"us\", \"us\"],"
"\"props_type\": \"histogram\"," "\"props_type\": \"histogram\","
"\"metadata\": {" "\"metadata\": {"
"\"i\": \"%dus\", \"duration\": \"%dh%d\"" "\"i\": \"%dus\", \"duration\": \"%dh%d\""
"}," "},"
"\"props\": [", interval, duration_hour, duration_minutes); "\"props\": [",
interval, duration_hour, duration_minutes);
max_hist_val = 0; max_hist_val = 0;
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
...@@ -293,42 +293,46 @@ static void print_histograms() { ...@@ -293,42 +293,46 @@ static void print_histograms() {
max_hist_val = 0; max_hist_val = 0;
for (int j = 0; j < MAX_HIST_VAL; j++) for (int j = 0; j < MAX_HIST_VAL; j++)
if(histograms[2][j]) if (histograms[2][j])
max_hist_val = j; max_hist_val = j;
min_hist_val = MAX_HIST_VAL - 1; min_hist_val = MAX_HIST_VAL - 1;
for (int j = MAX_HIST_VAL - 1; j >= 0; j--) for (int j = MAX_HIST_VAL - 1; j >= 0; j--)
if(histograms[2][j]) if (histograms[2][j])
min_hist_val = j; min_hist_val = j;
if(!enable_timestamps) if (!enable_timestamps)
printf("{\"measure_sets\": [{"); printf("{\"measure_sets\": [{");
else else
printf( "]}, {"); printf("]}, {");
printf( "\"measure_type\": \"packet_jitter\"," printf("\"measure_type\": \"packet_jitter\","
"\"props_names\": [\"jitter\"]," "\"props_names\": [\"jitter\"],"
"\"units\": [\"us\"]," "\"units\": [\"us\"],"
"\"props_type\": \"histogram\"," "\"props_type\": \"histogram\","
"\"middle\": \"%d\"," "\"middle\": \"%d\","
"\"metadata\": {" "\"metadata\": {"
"\"i\": \"%dus\", \"duration\": \"%dh%d\"" "\"i\": \"%dus\", \"duration\": \"%dh%d\""
"}," "},"
"\"props\": [[", MAX_HIST_VAL / 2 - min_hist_val, "\"props\": [[",
interval, MAX_HIST_VAL / 2 - min_hist_val,
duration_hour, interval,
duration_minutes); duration_hour,
duration_minutes);
for (int j = min_hist_val; j < max_hist_val; j++) for (int j = min_hist_val; j < max_hist_val; j++)
printf("%" PRIi64 "%s", histograms[2][j], (j + 1 < max_hist_val ? ", " : "")); printf("%" PRIi64 "%s", histograms[2][j], (j + 1 < max_hist_val ? ", " : ""));
printf( "]]}]}\n"); printf("]]}]}\n");
} }
static void sighand(int sig_num) {
static void sigint_handler(int sig_num) {
(void)sig_num; (void)sig_num;
if (enable_histograms)
print_histograms(); print_histograms();
if (param->stats.lost_packets)
fprintf(stderr, "%d packets were lost\n", param->stats.lost_packets);
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
} }
......
#define _GNU_SOURCE #define _GNU_SOURCE
#include <inttypes.h> #include <inttypes.h>
#include <signal.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#include "utilities.h" #include "utilities.h"
void (*previous_handlers[NSIG])(int);
static void (*sighand)(int);
uint64_t ts_to_uint(struct timespec t) { uint64_t ts_to_uint(struct timespec t) {
return t.tv_sec * NSEC_PER_SEC + t.tv_nsec; return t.tv_sec * NSEC_PER_SEC + t.tv_nsec;
} }
...@@ -28,3 +34,32 @@ uint64_t calcdiff_ns(struct timespec t1, struct timespec t2) { ...@@ -28,3 +34,32 @@ uint64_t calcdiff_ns(struct timespec t1, struct timespec t2) {
uint64_t max(uint64_t a, uint64_t b) { return a > b ? a : b; } uint64_t max(uint64_t a, uint64_t b) { return a > b ? a : b; }
uint64_t min(uint64_t a, uint64_t b) { return a < b ? a : b; } uint64_t min(uint64_t a, uint64_t b) { return a < b ? a : b; }
static void sighand_wrapper(int sig) {
// If we get un unexpected signal, report it, if not print the histogram
if (sig == SIGINT || sig == SIGTERM)
(*sighand)(sig); // Will print the histogram
else
printf("Uknown signal interrupt: %s (%d)\n", strsignal(sig), sig);
// Execute the default handler
if (previous_handlers[sig] == SIG_DFL) {
signal(sig, SIG_DFL);
raise(sig);
} else if (previous_handlers[sig] == SIG_IGN) {
return;
} else {
(*previous_handlers[sig])(sig);
}
}
void init_signals(void (*_sighand)(int), int enable_histograms) {
sighand = _sighand;
if (enable_histograms)
for (int i = 0; i < NSIG; i++)
signal(i, sighand_wrapper);
}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
#define _GNU_SOURCE #define _GNU_SOURCE
#include <inttypes.h> #include <inttypes.h>
#include <signal.h>
#include <stdint.h> #include <stdint.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
...@@ -27,4 +28,8 @@ uint64_t calcdiff_ns(struct timespec t1, struct timespec t2); ...@@ -27,4 +28,8 @@ uint64_t calcdiff_ns(struct timespec t1, struct timespec t2);
uint64_t max(uint64_t a, uint64_t b); uint64_t max(uint64_t a, uint64_t b);
uint64_t min(uint64_t a, uint64_t b); uint64_t min(uint64_t a, uint64_t b);
void init_signals(void (*_sighand)(int), int enable_histograms);
extern void (*previous_handlers[NSIG])(int);
#endif #endif
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment