text: /*
   * lws Generic Metrics
   *
   * Copyright (C) 2019 - 2021 Andy Green <andy@warmcat.com>
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to
   * deal in the Software without restriction, including without limitation the
   * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
   * sell copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
   * IN THE SOFTWARE.
   */
  
  #include "private-lib-core.h"
  #include <assert.h>
  
  int
  lws_metrics_tag_add(lws_dll2_owner_t *owner, const char *name, const char *val)
  {
  	size_t vl = strlen(val);
  	lws_metrics_tag_t *tag;
  
  	// lwsl_notice("%s: adding %s=%s\n", __func__, name, val);
  
  	/*
  	 * Remove (in order to replace) any existing tag of same name
  	 */
  
  	lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) {
  		tag = lws_container_of(d, lws_metrics_tag_t, list);
  
  		if (!strcmp(name, tag->name)) {
  			lws_dll2_remove(&tag->list);
  			lws_free(tag);
  			break;
  		}
  
  	} lws_end_foreach_dll(d);
  
  	/*
  	 * Create the new tag
  	 */
  
  	tag = lws_malloc(sizeof(*tag) + vl + 1, __func__);
  	if (!tag)
  		return 1;
  
  	lws_dll2_clear(&tag->list);
  	tag->name = name;
  	memcpy(&tag[1], val, vl + 1);
  
  	lws_dll2_add_tail(&tag->list, owner);
  
  	return 0;
  }
  
  int
  lws_metrics_tag_wsi_add(struct lws *wsi, const char *name, const char *val)
  {
  	__lws_lc_tag(NULL, &wsi->lc, "|%s", val);
  
  	return lws_metrics_tag_add(&wsi->cal_conn.mtags_owner, name, val);
  }
  
  #if defined(LWS_WITH_SECURE_STREAMS)
  int
  lws_metrics_tag_ss_add(struct lws_ss_handle *ss, const char *name, const char *val)
  {
  	__lws_lc_tag(NULL, &ss->lc, "|%s", val);
  	return lws_metrics_tag_add(&ss->cal_txn.mtags_owner, name, val);
  }
  #if defined(LWS_WITH_SECURE_STREAMS)
  int
  lws_metrics_tag_sspc_add(struct lws_sspc_handle *sspc, const char *name,
  			 const char *val)
  {
  	__lws_lc_tag(NULL, &sspc->lc, "|%s", val);
  	return lws_metrics_tag_add(&sspc->cal_txn.mtags_owner, name, val);
  }
  #endif
  #endif
  
  void
  lws_metrics_tags_destroy(lws_dll2_owner_t *owner)
  {
  	lws_metrics_tag_t *t;
  
  	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, owner->head) {
  		t = lws_container_of(d, lws_metrics_tag_t, list);
  
  		lws_dll2_remove(&t->list);
  		lws_free(t);
  
  	} lws_end_foreach_dll_safe(d, d1);
  }
  
  size_t
  lws_metrics_tags_serialize(lws_dll2_owner_t *owner, char *buf, size_t len)
  {
  	char *end = buf + len - 1, *p = buf;
  	lws_metrics_tag_t *t;
  
  	lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) {
  		t = lws_container_of(d, lws_metrics_tag_t, list);
  
  		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
  				  "%s=\"%s\"", t->name, (const char *)&t[1]);
  
  		if (d->next && p + 2 < end)
  			*p++ = ',';
  
  	} lws_end_foreach_dll(d);
  
  	*p = '\0';
  
  	return lws_ptr_diff_size_t(p, buf);
  }
  
  const char *
  lws_metrics_tag_get(lws_dll2_owner_t *owner, const char *name)
  {
  	lws_metrics_tag_t *t;
  
  	lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) {
  		t = lws_container_of(d, lws_metrics_tag_t, list);
  
  		if (!strcmp(name, t->name))
  			return (const char *)&t[1];
  
  	} lws_end_foreach_dll(d);
  
  	return NULL;
  }
  
  static int
  lws_metrics_dump_cb(lws_metric_pub_t *pub, void *user);
  
  static void
  lws_metrics_report_and_maybe_clear(struct lws_context *ctx, lws_metric_pub_t *pub)
  {
  	if (!pub->us_first || pub->us_last == pub->us_dumped)
  		return;
  
  	lws_metrics_dump_cb(pub, ctx);
  }
  
  static void
  lws_metrics_periodic_cb(lws_sorted_usec_list_t *sul)
  {
  	lws_metric_policy_dyn_t *dmp = lws_container_of(sul,
  						lws_metric_policy_dyn_t, sul);
  	struct lws_context *ctx = lws_container_of(dmp->list.owner,
  					struct lws_context, owner_mtr_dynpol);
  
  	if (!ctx->system_ops || !ctx->system_ops->metric_report)
  		return;
  
  	lws_start_foreach_dll(struct lws_dll2 *, d, dmp->owner.head) {
  		lws_metric_t *mt = lws_container_of(d, lws_metric_t, list);
  		lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt);
  
  		lws_metrics_report_and_maybe_clear(ctx, pub);
  
  	} lws_end_foreach_dll(d);
  
  #if defined(LWS_WITH_SYS_SMD) && defined(LWS_WITH_SECURE_STREAMS)
  	(void)lws_smd_msg_printf(ctx, LWSSMDCL_METRICS,
  				 "{\"dump\":\"%s\",\"ts\":%lu}",
  				   dmp->policy->name,
  				   (long)ctx->last_policy);
  #endif
  
  	if (dmp->policy->us_schedule)
  		lws_sul_schedule(ctx, 0, &dmp->sul,
  				 lws_metrics_periodic_cb,
  				 (lws_usec_t)dmp->policy->us_schedule);
  }
  
  /*
   * Policies are in two pieces, a const policy and a dynamic part that contains
   * lists and sul timers for the policy etc.  This creates a dynmic part
   * corresponding to the static part.
   *
   * Metrics can exist detached from being bound to any policy about how to
   * report them, these are collected but not reported unless they later become
   * bound to a reporting policy dynamically.
   */
  
  lws_metric_policy_dyn_t *
  lws_metrics_policy_dyn_create(struct lws_context *ctx,
  			      const lws_metric_policy_t *po)
  {
  	lws_metric_policy_dyn_t *dmet;
  
  	dmet = lws_zalloc(sizeof(*dmet), __func__);
  	if (!dmet)
  		return NULL;
  
  	dmet->policy = po;
  	lws_dll2_add_tail(&dmet->list, &ctx->owner_mtr_dynpol);
  
  	if (po->us_schedule)
  		lws_sul_schedule(ctx, 0, &dmet->sul,
  				 lws_metrics_periodic_cb,
  				 (lws_usec_t)po->us_schedule);
  
  	return dmet;
  }
  
  /*
   * Get a dynamic metrics policy from the const one, may return NULL if OOM
   */
  
  lws_metric_policy_dyn_t *
  lws_metrics_policy_get_dyn(struct lws_context *ctx,
  			   const lws_metric_policy_t *po)
  {
  	lws_start_foreach_dll(struct lws_dll2 *, d, ctx->owner_mtr_dynpol.head) {
  		lws_metric_policy_dyn_t *dm =
  			lws_container_of(d, lws_metric_policy_dyn_t, list);
  
  		if (dm->policy == po)
  			return dm;
  
  	} lws_end_foreach_dll(d);
  
  	/*
  	 * no dyn policy part for this const policy --> create one
  	 *
  	 * We want a dynamic part for listing metrics that bound to the policy
  	 */
  
  	return lws_metrics_policy_dyn_create(ctx, po);
  }
  
  static int
  lws_metrics_check_in_policy(const char *polstring, const char *name)
  {
  	struct lws_tokenize ts;
  
  	memset(&ts, 0, sizeof(ts));
  
  	ts.start = polstring;
  	ts.len = strlen(polstring);
  	ts.flags = (uint16_t)(LWS_TOKENIZE_F_MINUS_NONTERM |
  			      LWS_TOKENIZE_F_ASTERISK_NONTERM |
  			      LWS_TOKENIZE_F_COMMA_SEP_LIST |
  			      LWS_TOKENIZE_F_NO_FLOATS |
  			      LWS_TOKENIZE_F_DOT_NONTERM);
  
  	do {
  		ts.e = (int8_t)lws_tokenize(&ts);
  
  		if (ts.e == LWS_TOKZE_TOKEN) {
  			if (!lws_strcmp_wildcard(ts.token, ts.token_len, name))
  				/* yes, we are mentioned in this guy's policy */
  				return 0;
  		}
  	} while (ts.e > 0);
  
  	/* no, this policy doesn't apply to a metric with our name */
  
  	return 1;
  }
  
  static const lws_metric_policy_t *
  lws_metrics_find_policy(struct lws_context *ctx, const char *name)
  {
  	const lws_metric_policy_t *mp = ctx->metrics_policies;
  
  	if (!mp) {
  #if defined(LWS_WITH_SECURE_STREAMS)
  		if (ctx->pss_policies)
  			mp = ctx->pss_policies->metrics;
  #endif
  		if (!mp)
  			return NULL;
  	}
  
  	while (mp) {
  		if (mp->report && !lws_metrics_check_in_policy(mp->report, name))
  			return mp;
  
  		mp = mp->next;
  	}
  
  	return NULL;
  }
  
  /*
   * Create a lws_metric_t, bind to a named policy if possible (or add to the
   * context list of unbound metrics) and set its lws_system
   * idx.  The metrics objects themselves are typically composed into other
   * objects and are well-known composed members of them.
   */
  
  lws_metric_t *
  lws_metric_create(struct lws_context *ctx, uint8_t flags, const char *name)
  {
  	const lws_metric_policy_t *po;
  	lws_metric_policy_dyn_t *dmp;
  	lws_metric_pub_t *pub;
  	lws_metric_t *mt;
  	char pname[32];
  	size_t nl;
  
  	if (ctx->metrics_prefix) {
  
  		/*
  		 * In multi-process case, we want to prefix metrics from this
  		 * process / context with a string distinguishing which
  		 * application they came from
  		 */
  
  		nl = (size_t)lws_snprintf(pname, sizeof(pname) - 1, "%s.%s",
  				  ctx->metrics_prefix, name);
  		name = pname;
  	} else
  		nl = strlen(name);
  
  	mt = (lws_metric_t *)lws_zalloc(sizeof(*mt) /* private */ +
  					sizeof(lws_metric_pub_t) +
  					nl + 1 /* copy of metric name */,
  					__func__);
  	if (!mt)
  		return NULL;
  
  	pub = lws_metrics_priv_to_pub(mt);
  	pub->name = (char *)pub + sizeof(lws_metric_pub_t);
  	memcpy((char *)pub->name, name, nl + 1);
  	pub->flags = flags;
  
  	/* after these common members, we have to use the right type */
  
  	if (!(flags & LWSMTFL_REPORT_HIST)) {
  		/* anything is smaller or equal to this */
  		pub->u.agg.min = ~(u_mt_t)0;
  		pub->us_first = lws_now_usecs();
  	}
  
  	mt->ctx = ctx;
  
  	/*
  	 * Let's see if we can bind to a reporting policy straight away
  	 */
  
  	po = lws_metrics_find_policy(ctx, name);
  	if (po) {
  		dmp = lws_metrics_policy_get_dyn(ctx, po);
  		if (dmp) {
  			lwsl_notice("%s: metpol %s\n", __func__, name);
  			lws_dll2_add_tail(&mt->list, &dmp->owner);
  
  			return 0;
  		}
  	}
  
  	/*
  	 * If not, well, let's go on without and maybe later at runtime, he'll
  	 * get interested in us and apply a reporting policy
  	 */
  
  	lws_dll2_add_tail(&mt->list, &ctx->owner_mtr_no_pol);
  
  	return mt;
  }
  
  /*
   * If our metric is bound to a reporting policy, return a pointer to it,
   * otherwise NULL
   */
  
  const lws_metric_policy_t *
  lws_metric_get_policy(lws_metric_t *mt)
  {
  	lws_metric_policy_dyn_t *dp;
  
  	/*
  	 * Our metric must either be on the "no policy" context list or
  	 * listed by the dynamic part of the policy it is bound to
  	 */
  	assert(mt->list.owner);
  
  	if ((char *)mt->list.owner >= (char *)mt->ctx &&
  	    (char *)mt->list.owner < (char *)mt->ctx + sizeof(struct lws_context))
  		/* we are on the "no policy" context list */
  		return NULL;
  
  	/* we are listed by a dynamic policy owner */
  
  	dp = lws_container_of(mt->list.owner, lws_metric_policy_dyn_t, owner);
  
  	/* return the const policy the dynamic policy represents */
  
  	return dp->policy;
  }
  
  void
  lws_metric_rebind_policies(struct lws_context *ctx)
  {
  	const lws_metric_policy_t *po;
  	lws_metric_policy_dyn_t *dmp;
  
  	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
  				   ctx->owner_mtr_no_pol.head) {
  		lws_metric_t *mt = lws_container_of(d, lws_metric_t, list);
  		lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt);
  
  		po = lws_metrics_find_policy(ctx, pub->name);
  		if (po) {
  			dmp = lws_metrics_policy_get_dyn(ctx, po);
  			if (dmp) {
  				lwsl_info("%s: %s <- pol %s\n", __func__,
  						pub->name, po->name);
  				lws_dll2_remove(&mt->list);
  				lws_dll2_add_tail(&mt->list, &dmp->owner);
  			}
  		} else
  			lwsl_debug("%s: no pol for %s\n", __func__, pub->name);
  
  	} lws_end_foreach_dll_safe(d, d1);
  }
  
  int
  lws_metric_destroy(lws_metric_t **pmt, int keep)
  {
  	lws_metric_t *mt = *pmt;
  	lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt);
  
  	if (!mt)
  		return 0;
  
  	lws_dll2_remove(&mt->list);
  
  	if (keep) {
  		lws_dll2_add_tail(&mt->list, &mt->ctx->owner_mtr_no_pol);
  
  		return 0;
  	}
  
  	if (pub->flags & LWSMTFL_REPORT_HIST) {
  		lws_metric_bucket_t *b = pub->u.hist.head, *b1;
  
  		pub->u.hist.head = NULL;
  
  		while (b) {
  			b1 = b->next;
  			lws_free(b);
  			b = b1;
  		}
  	}
  
  	lws_free(mt);
  	*pmt = NULL;
  
  	return 0;
  }
  
  /*
   * Allow an existing metric to have its reporting policy changed at runtime
   */
  
  int
  lws_metric_switch_policy(lws_metric_t *mt, const char *polname)
  {
  	const lws_metric_policy_t *po;
  	lws_metric_policy_dyn_t *dmp;
  
  	po = lws_metrics_find_policy(mt->ctx, polname);
  	if (!po)
  		return 1;
  
  	dmp = lws_metrics_policy_get_dyn(mt->ctx, po);
  	if (!dmp)
  		return 1;
  
  	lws_dll2_remove(&mt->list);
  	lws_dll2_add_tail(&mt->list, &dmp->owner);
  
  	return 0;
  }
  
  /*
   * If keep is set, don't destroy existing metrics objects, just detach them
   * from the policy being deleted and keep track of them on ctx->
   * owner_mtr_no_pol
   */
  
  void
  lws_metric_policy_dyn_destroy(lws_metric_policy_dyn_t *dm, int keep)
  {
  	lws_sul_cancel(&dm->sul);
  
  	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, dm->owner.head) {
  		lws_metric_t *m = lws_container_of(d, lws_metric_t, list);
  
  		lws_metric_destroy(&m, keep);
  
  	} lws_end_foreach_dll_safe(d, d1);
  
  	lws_sul_cancel(&dm->sul);
  
  	lws_dll2_remove(&dm->list);
  	lws_free(dm);
  }
  
  /*
   * Destroy all dynamic metrics policies, deinit any metrics still using them
   */
  
  void
  lws_metrics_destroy(struct lws_context *ctx)
  {
  	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
  				   ctx->owner_mtr_dynpol.head) {
  		lws_metric_policy_dyn_t *dm =
  			lws_container_of(d, lws_metric_policy_dyn_t, list);
  
  		lws_metric_policy_dyn_destroy(dm, 0); /* don't keep */
  
  	} lws_end_foreach_dll_safe(d, d1);
  
  	/* destroy metrics with no current policy too... */
  
  	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
  				   ctx->owner_mtr_no_pol.head) {
  		lws_metric_t *mt = lws_container_of(d, lws_metric_t, list);
  
  		lws_metric_destroy(&mt, 0); /* don't keep */
  
  	} lws_end_foreach_dll_safe(d, d1);
  
  	/* ... that's the whole allocated metrics footprint gone... */
  }
  
  int
  lws_metrics_hist_bump_(lws_metric_pub_t *pub, const char *name)
  {
  	lws_metric_bucket_t *buck = pub->u.hist.head;
  	size_t nl = strlen(name);
  	char *nm;
  
  	if (!(pub->flags & LWSMTFL_REPORT_HIST)) {
  		lwsl_err("%s: %s not histogram: flags %d\n", __func__,
  				pub->name, pub->flags);
  		assert(0);
  	}
  	assert(nl < 255);
  
  	pub->us_last = lws_now_usecs();
  	if (!pub->us_first)
  		pub->us_first = pub->us_last;
  
  	while (buck) {
  		if (lws_metric_bucket_name_len(buck) == nl &&
  		    !strcmp(name, lws_metric_bucket_name(buck))) {
  			buck->count++;
  			goto happy;
  		}
  		buck = buck->next;
  	}
  
  	buck = lws_malloc(sizeof(*buck) + nl + 2, __func__);
  	if (!buck)
  		return 1;
  
  	nm = (char *)buck + sizeof(*buck);
  	/* length byte at beginning of name, avoid struct alignment overhead */
  	*nm = (char)nl;
  	memcpy(nm + 1, name, nl + 1);
  
  	buck->next = pub->u.hist.head;
  	pub->u.hist.head = buck;
  	buck->count = 1;
  	pub->u.hist.list_size++;
  
  happy:
  	pub->u.hist.total_count++;
  
  	return 0;
  }
  
  int
  lws_metrics_hist_bump_describe_wsi(struct lws *wsi, lws_metric_pub_t *pub,
  				   const char *name)
  {
  	char desc[192], d1[48], *p = desc, *end = desc + sizeof(desc);
  
  #if defined(LWS_WITH_SECURE_STREAMS)
  #if defined(LWS_WITH_SECURE_STREAMS_PROXY_API)
  	if (wsi->client_bound_sspc) {
  		lws_sspc_handle_t *h = (lws_sspc_handle_t *)wsi->a.opaque_user_data;
  		if (h)
  			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "ss=\"%s\",",
  				  h->ssi.streamtype);
  	} else
  		if (wsi->client_proxy_onward) {
  			lws_ss_handle_t *h = (lws_ss_handle_t *)wsi->a.opaque_user_data;
  			struct conn *conn = h->conn_if_sspc_onw;
  
  			if (conn && conn->ss)
  				p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
  						  "ss=\"%s\",",
  						  conn->ss->info.streamtype);
  		} else
  #endif
  	if (wsi->for_ss) {
  		lws_ss_handle_t *h = (lws_ss_handle_t *)wsi->a.opaque_user_data;
  		if (h)
  			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "ss=\"%s\",",
  				  h->info.streamtype);
  	}
  #endif
  
  #if defined(LWS_WITH_CLIENT)
  	if (wsi->stash && wsi->stash->cis[CIS_HOST])
  		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "hostname=\"%s\",",
  				wsi->stash->cis[CIS_HOST]);
  #endif
  
  	lws_sa46_write_numeric_address(&wsi->sa46_peer, d1, sizeof(d1));
  	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "peer=\"%s\",", d1);
  
  	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "%s", name);
  
  	lws_metrics_hist_bump_(pub, desc);
  
  	return 0;
  }
  
  int
  lws_metrics_foreach(struct lws_context *ctx, void *user,
  		    int (*cb)(lws_metric_pub_t *pub, void *user))
  {
  	int n;
  
  	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
  				   ctx->owner_mtr_no_pol.head) {
  		lws_metric_t *mt = lws_container_of(d, lws_metric_t, list);
  
  		n = cb(lws_metrics_priv_to_pub(mt), user);
  		if (n)
  			return n;
  
  	} lws_end_foreach_dll_safe(d, d1);
  
  	lws_start_foreach_dll_safe(struct lws_dll2 *, d2, d3,
  				   ctx->owner_mtr_dynpol.head) {
  		lws_metric_policy_dyn_t *dm =
  			lws_container_of(d2, lws_metric_policy_dyn_t, list);
  
  		lws_start_foreach_dll_safe(struct lws_dll2 *, e, e1,
  					   dm->owner.head) {
  
  			lws_metric_t *mt = lws_container_of(e, lws_metric_t, list);
  
  			n = cb(lws_metrics_priv_to_pub(mt), user);
  			if (n)
  				return n;
  
  		} lws_end_foreach_dll_safe(e, e1);
  
  	} lws_end_foreach_dll_safe(d2, d3);
  
  	return 0;
  }
  
  static int
  lws_metrics_dump_cb(lws_metric_pub_t *pub, void *user)
  {
  	struct lws_context *ctx = (struct lws_context *)user;
  	int n;
  
  	if (!ctx->system_ops || !ctx->system_ops->metric_report)
  		return 0;
  
  	/*
  	 * return nonzero to reset stats
  	 */
  
  	n = ctx->system_ops->metric_report(pub);
  
  	/* track when we dumped it... */
  
  	pub->us_first = pub->us_dumped = lws_now_usecs();
  	pub->us_last = 0;
  
  	if (!n)
  		return 0;
  
  	/* ... and clear it back to 0 */
  
  	if (pub->flags & LWSMTFL_REPORT_HIST) {
  		lws_metric_bucket_t *b = pub->u.hist.head, *b1;
  		pub->u.hist.head = NULL;
  
  		while (b) {
  			b1 = b->next;
  			lws_free(b);
  			b = b1;
  		}
  		pub->u.hist.total_count = 0;
  		pub->u.hist.list_size = 0;
  	} else
  		memset(&pub->u.agg, 0, sizeof(pub->u.agg));
  
  	return 0;
  }
  
  void
  lws_metrics_dump(struct lws_context *ctx)
  {
  	lws_metrics_foreach(ctx, ctx, lws_metrics_dump_cb);
  }
  
  static int
  _lws_metrics_format(lws_metric_pub_t *pub, lws_usec_t now, int gng,
  		    char *buf, size_t len)
  {
  	const lws_humanize_unit_t *schema = humanize_schema_si;
  	char *end = buf + len - 1, *obuf = buf;
  
  	if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US)
  		schema = humanize_schema_us;
  
  	if (!(pub->flags & LWSMTFL_REPORT_MEAN)) {
  		/* only the sum is meaningful */
  		if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US) {
  
  			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), " %u, ",
  						(unsigned int)pub->u.agg.count[gng]);
  
  			buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf),
  					    (uint64_t)pub->u.agg.sum[gng],
  					    humanize_schema_us);
  
  			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), " / ");
  
  			buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf),
  					    (uint64_t)(now - pub->us_first),
  					    humanize_schema_us);
  
  			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
  					    " (%d%%)", (int)((100 * pub->u.agg.sum[gng]) /
  						(unsigned long)(now - pub->us_first)));
  		} else {
  			/* it's a monotonic ordinal, like total tx */
  			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "(%u) ",
  					(unsigned int)pub->u.agg.count[gng]);
  			buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf),
  					    (uint64_t)pub->u.agg.sum[gng],
  					    humanize_schema_si);
  		}
  
  	} else {
  		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "%u, mean: ", (unsigned int)pub->u.agg.count[gng]);
  		/* the average over the period is meaningful */
  		buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf),
  				    (uint64_t)(pub->u.agg.count[gng] ?
  					 pub->u.agg.sum[gng] / pub->u.agg.count[gng] : 0),
  				    schema);
  	}
  
  	return lws_ptr_diff(buf, obuf);
  }
  
  int
  lws_metrics_format(lws_metric_pub_t *pub, lws_metric_bucket_t **sub, char *buf, size_t len)
  {
  	char *end = buf + len - 1, *obuf = buf;
  	lws_usec_t t = lws_now_usecs();
  	const lws_humanize_unit_t *schema = humanize_schema_si;
  
  	if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US)
  		schema = humanize_schema_us;
  
  	if (pub->flags & LWSMTFL_REPORT_HIST) {
  
  		if (*sub == NULL)
  			return 0;
  
  		if (*sub) {
  			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
  					    "%s{%s} %llu", pub->name,
  					    lws_metric_bucket_name(*sub),
  					    (unsigned long long)(*sub)->count);
  
  			*sub = (*sub)->next;
  		}
  
  		goto happy;
  	}
  
  	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "%s: ",
  				pub->name);
  
  	if (!pub->u.agg.count[METRES_GO] && !pub->u.agg.count[METRES_NOGO])
  		return 0;
  
  	if (pub->u.agg.count[METRES_GO]) {
  		if (!(pub->flags & LWSMTFL_REPORT_ONLY_GO))
  			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
  					    "Go: ");
  		buf += _lws_metrics_format(pub, t, METRES_GO, buf,
  					   lws_ptr_diff_size_t(end, buf));
  	}
  
  	if (!(pub->flags & LWSMTFL_REPORT_ONLY_GO) && pub->u.agg.count[METRES_NOGO]) {
  		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", NoGo: ");
  		buf += _lws_metrics_format(pub, t, METRES_NOGO, buf,
  					   lws_ptr_diff_size_t(end, buf));
  	}
  
  	if (pub->flags & LWSMTFL_REPORT_MEAN) {
  		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", min: ");
  		buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), pub->u.agg.min,
  				    schema);
  		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", max: ");
  		buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), pub->u.agg.max,
  				    schema);
  	}
  
  happy:
  	if (pub->flags & LWSMTFL_REPORT_HIST)
  		return 1;
  
  	*sub = NULL;
  
  	return lws_ptr_diff(buf, obuf);
  }
  
  /*
   * We want to, at least internally, record an event... depending on the policy,
   * that might cause us to call through to the lws_system apis, or just update
   * our local stats about it and dump at the next periodic chance (also set by
   * the policy)
   */
  
  void
  lws_metric_event(lws_metric_t *mt, char go_nogo, u_mt_t val)
  {
  	lws_metric_pub_t *pub;
  
  	assert((go_nogo & 0xfe) == 0);
  
  	if (!mt)
  		return;
  
  	pub = lws_metrics_priv_to_pub(mt);
  	assert(!(pub->flags & LWSMTFL_REPORT_HIST));
  
  	pub->us_last = lws_now_usecs();
  	if (!pub->us_first)
  		pub->us_first = pub->us_last;
  	pub->u.agg.count[(int)go_nogo]++;
  	pub->u.agg.sum[(int)go_nogo] += val;
  	if (val > pub->u.agg.max)
  		pub->u.agg.max = val;
  	if (val < pub->u.agg.min)
  		pub->u.agg.min = val;
  
  	if (pub->flags & LWSMTFL_REPORT_OOB)
  		lws_metrics_report_and_maybe_clear(mt->ctx, pub);
  }
  
  
  void
  lws_metrics_hist_bump_priv_tagged(lws_metric_pub_t *mt, lws_dll2_owner_t *tow,
  				  lws_dll2_owner_t *tow2)
  {
  	char qual[192];
  	size_t p;
  
  	p = lws_metrics_tags_serialize(tow, qual, sizeof(qual));
  	if (tow2)
  		lws_metrics_tags_serialize(tow2, qual + p,
  				sizeof(qual) - p);
  
  	lws_metrics_hist_bump(mt, qual);
  }
  