Subject: Re: [PATCH iproute2] ss: Render buffer to output every time a number of chunks are allocated

From: Stefano Brivio <[email-masked]>
Date: Wed, 20 Feb 2019 17:58:08 +0100

Hi Stephen,

I think this patch is reasonably tested now. Eric, who reported the
original issue, is also satisfied with it. Is there any issue with it?

-- 
Stefano

On Thu, 14 Feb 2019 01:58:32 +0100
Stefano Brivio <[email-masked]> wrote:

> Eric reported that, with 10 million sockets, ss -emoi (about 1000 bytes
> output per socket) can easily lead to OOM (buffer would grow to 10GB of
> memory).
> 
> Limit the maximum size of the buffer to five chunks, 1M each. Render and
> flush buffers whenever we reach that.
> 
> This might make the resulting blocks slightly unaligned between them, with
> occasional loss of readability on lines occurring every 5k to 50k sockets
> approximately. Something like (from ss -tu):
> 
> [...]
> CLOSE-WAIT   32       0           192.168.1.50:35232           10.0.0.1:https
> ESTAB        0        0           192.168.1.50:53820           10.0.0.1:https
> ESTAB       0        0           192.168.1.50:46924            10.0.0.1:https
> CLOSE-WAIT  32       0           192.168.1.50:35228            10.0.0.1:https
> [...]
> 
> However, I don't actually expect any human user to scroll through that
> amount of sockets, so readability should be preserved when it matters.
> 
> The bulk of the diffstat comes from moving field_next() around, as we now
> call render() from it. Functionally, this is implemented by six lines of
> code, most of them in field_next().
> 
> Reported-by: Eric Dumazet <[email-masked]>
> Fixes: 691bd854bf4a ("ss: Buffer raw fields first, then render them as a table")
> Signed-off-by: Stefano Brivio <[email-masked]>
> ---
> Eric, it would be nice if you could test this with your bazillion sockets,
> I checked this with -emoi and "only" 500,000 sockets.
> 
>  misc/ss.c | 68 ++++++++++++++++++++++++++++++++-----------------------
>  1 file changed, 40 insertions(+), 28 deletions(-)
> 
> diff --git a/misc/ss.c b/misc/ss.c
> index 9e821faf0d31..28bdcba72d73 100644
> --- a/misc/ss.c
> +++ b/misc/ss.c
> @@ -52,7 +52,8 @@
>  #include <linux/tipc_sockets_diag.h>
>  
>  #define MAGIC_SEQ 123456
> -#define BUF_CHUNK (1024 * 1024)
> +#define BUF_CHUNK (1024 * 1024)	/* Buffer chunk allocation size */
> +#define BUF_CHUNKS_MAX 5	/* Maximum number of allocated buffer chunks */
>  #define LEN_ALIGN(x) (((x) + 1) & ~1)
>  
>  #define DIAG_REQUEST(_req, _r)						    \
> @@ -176,6 +177,7 @@ static struct {
>  	struct buf_token *cur;	/* Position of current token in chunk */
>  	struct buf_chunk *head;	/* First chunk */
>  	struct buf_chunk *tail;	/* Current chunk */
> +	int chunks;		/* Number of allocated chunks */
>  } buffer;
>  
>  static const char *TCP_PROTO = "tcp";
> @@ -936,6 +938,8 @@ static struct buf_chunk *buf_chunk_new(void)
>  
>  	new->end = buffer.cur->data;
>  
> +	buffer.chunks++;
> +
>  	return new;
>  }
>  
> @@ -1080,33 +1084,6 @@ static int field_is_last(struct column *f)
>  	return f - columns == COL_MAX - 1;
>  }
>  
> -static void field_next(void)
> -{
> -	field_flush(current_field);
> -
> -	if (field_is_last(current_field))
> -		current_field = columns;
> -	else
> -		current_field++;
> -}
> -
> -/* Walk through fields and flush them until we reach the desired one */
> -static void field_set(enum col_id id)
> -{
> -	while (id != current_field - columns)
> -		field_next();
> -}
> -
> -/* Print header for all non-empty columns */
> -static void print_header(void)
> -{
> -	while (!field_is_last(current_field)) {
> -		if (!current_field->disabled)
> -			out("%s", current_field->header);
> -		field_next();
> -	}
> -}
> -
>  /* Get the next available token in the buffer starting from the current token */
>  static struct buf_token *buf_token_next(struct buf_token *cur)
>  {
> @@ -1132,6 +1109,7 @@ static void buf_free_all(void)
>  		free(tmp);
>  	}
>  	buffer.head = NULL;
> +	buffer.chunks = 0;
>  }
>  
>  /* Get current screen width, default to 80 columns if TIOCGWINSZ fails */
> @@ -1294,6 +1272,40 @@ static void render(void)
>  	current_field = columns;
>  }
>  
> +/* Move to next field, and render buffer if we reached the maximum number of
> + * chunks, at the last field in a line.
> + */
> +static void field_next(void)
> +{
> +	if (field_is_last(current_field) && buffer.chunks >= BUF_CHUNKS_MAX) {
> +		render();
> +		return;
> +	}
> +
> +	field_flush(current_field);
> +	if (field_is_last(current_field))
> +		current_field = columns;
> +	else
> +		current_field++;
> +}
> +
> +/* Walk through fields and flush them until we reach the desired one */
> +static void field_set(enum col_id id)
> +{
> +	while (id != current_field - columns)
> +		field_next();
> +}
> +
> +/* Print header for all non-empty columns */
> +static void print_header(void)
> +{
> +	while (!field_is_last(current_field)) {
> +		if (!current_field->disabled)
> +			out("%s", current_field->header);
> +		field_next();
> +	}
> +}
> +
>  static void sock_state_print(struct sockstat *s)
>  {
>  	const char *sock_name;