[v5,2/4] eal: allow applications to report their cpu usage

Message ID 20221216102109.64142-3-rjarry@redhat.com (mailing list archive)
State Superseded, archived
Delegated to: David Marchand
Headers
Series lcore telemetry improvements |

Checks

Context Check Description
ci/checkpatch success coding style OK

Commit Message

Robin Jarry Dec. 16, 2022, 10:21 a.m. UTC
  Allow applications to register a callback that will be invoked in
rte_lcore_dump() and when requesting lcore info in the telemetry API.

The callback is expected to return the number of TSC cycles that have
passed since application start and the number of these cycles that were
spent doing busy work.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Acked-by: Morten Brørup <mb@smartsharesystems.com>
---
v4 -> v5:

The callback now takes a pointer to a rte_lcore_usage structure.
I chose not to include any API version tracking mechanism since the
unsupported/unused fields can simply be left to zero. This is only
telemetry after all.

 lib/eal/common/eal_common_lcore.c | 33 ++++++++++++++++++++++++++---
 lib/eal/include/rte_lcore.h       | 35 +++++++++++++++++++++++++++++++
 lib/eal/version.map               |  1 +
 3 files changed, 66 insertions(+), 3 deletions(-)
  

Comments

Morten Brørup Dec. 16, 2022, 10:47 a.m. UTC | #1
> From: Robin Jarry [mailto:rjarry@redhat.com]
> Sent: Friday, 16 December 2022 11.21
> 
> Allow applications to register a callback that will be invoked in
> rte_lcore_dump() and when requesting lcore info in the telemetry API.
> 
> The callback is expected to return the number of TSC cycles that have
> passed since application start and the number of these cycles that were
> spent doing busy work.
> 
> Signed-off-by: Robin Jarry <rjarry@redhat.com>
> Acked-by: Morten Brørup <mb@smartsharesystems.com>
> ---
> v4 -> v5:
> 
> The callback now takes a pointer to a rte_lcore_usage structure.
> I chose not to include any API version tracking mechanism since the
> unsupported/unused fields can simply be left to zero. This is only
> telemetry after all.

ACK to this decision, with a minor clarification to avoid any misinterpretation:

The callback should not modify (i.e. zero out) unsupported/unused fields.

The caller needs to clear the structure before calling the callback - because the callback might not use the updated size of the structure, if the application was written for an older DPDK version with a smaller structure. I can see you already do this. Consider adding a comment about it in the code.

[...]

>  static int
>  lcore_dump_cb(unsigned int lcore_id, void *arg)
>  {
>  	struct rte_config *cfg = rte_eal_get_configuration();
> -	char cpuset[RTE_CPU_AFFINITY_STR_LEN];
> +	char cpuset[RTE_CPU_AFFINITY_STR_LEN], usage_str[256];
> +	struct rte_lcore_usage usage;
> +	rte_lcore_usage_cb usage_cb;
>  	const char *role;
>  	FILE *f = arg;
>  	int ret;
> @@ -446,11 +457,19 @@ lcore_dump_cb(unsigned int lcore_id, void *arg)
>  		break;
>  	}
> 
> +	memset(&usage, 0, sizeof(usage));

I would move this memset() inside the below if-block.

> +	usage_str[0] = '\0';
> +	usage_cb = lcore_usage_cb;
> +	if (usage_cb != NULL && usage_cb(lcore_id, &usage) == 0) {

Move memset() inside here, and add comment:

+ /* The application's callback may not set all the fields in the structure, so clear it here. */
+ memset(&usage, 0, sizeof(usage));

> +		snprintf(usage_str, sizeof(usage_str), ", busy cycles
> %"PRIu64"/%"PRIu64,
> +			usage.busy_cycles, usage.total_cycles);
> +	}

[...]

> @@ -522,6 +543,12 @@ lcore_telemetry_info_cb(unsigned int lcore_id,
> void *arg)
>  		if (CPU_ISSET(cpu, &lcore_config[lcore_id].cpuset))
>  			rte_tel_data_add_array_int(cpuset, cpu);
>  	rte_tel_data_add_dict_container(info->d, "cpuset", cpuset, 0);
> +	memset(&usage, 0, sizeof(usage));
> +	usage_cb = lcore_usage_cb;
> +	if (usage_cb != NULL && usage_cb(lcore_id, &usage) == 0) {

Same comment as above: Move memset() inside here, and add a comment about why the structure is cleared here.

> +		rte_tel_data_add_dict_u64(info->d, "busy_cycles",
> usage.busy_cycles);
> +		rte_tel_data_add_dict_u64(info->d, "total_cycles",
> usage.total_cycles);
> +	}
> 
>  	return 0;
>  }
> diff --git a/lib/eal/include/rte_lcore.h b/lib/eal/include/rte_lcore.h
> index 6938c3fd7b81..a92313577355 100644
> --- a/lib/eal/include/rte_lcore.h
> +++ b/lib/eal/include/rte_lcore.h
> @@ -328,6 +328,41 @@ typedef int (*rte_lcore_iterate_cb)(unsigned int
> lcore_id, void *arg);
>  int
>  rte_lcore_iterate(rte_lcore_iterate_cb cb, void *arg);
> 
> +/**
> + * CPU usage statistics.
> + */
> +struct rte_lcore_usage {
> +	uint64_t total_cycles;
> +	/**< The total amount of time since application start, in TSC
> cycles. */
> +	uint64_t busy_cycles;
> +	/**< The amount of busy time since application start, in TSC
> cycles. */
> +};
> +
> +/**
> + * Callback to allow applications to report CPU usage.
> + *
> + * @param [in] lcore_id
> + *   The lcore to consider.
> + * @param [out] usage
> + *   Counters representing this lcore usage. This can never be NULL.
> + * @return
> + *   - 0 if fields in usage were updated successfully. The fields that
> the
> + *       application does not support should be left to their default
> value.

"should be left to their default value." -> "must not be modified."

> + *   - a negative value if the information is not available or if any
> error occurred.
> + */
> +typedef int (*rte_lcore_usage_cb)(unsigned int lcore_id, struct
> rte_lcore_usage *usage);
  
Konstantin Ananyev Dec. 22, 2022, 12:41 p.m. UTC | #2
> 
> Allow applications to register a callback that will be invoked in
> rte_lcore_dump() and when requesting lcore info in the telemetry API.
> 
> The callback is expected to return the number of TSC cycles that have
> passed since application start and the number of these cycles that were
> spent doing busy work.
> 
> Signed-off-by: Robin Jarry <rjarry@redhat.com>
> Acked-by: Morten Brørup <mb@smartsharesystems.com>
> ---
> v4 -> v5:
> 
> The callback now takes a pointer to a rte_lcore_usage structure.
> I chose not to include any API version tracking mechanism since the
> unsupported/unused fields can simply be left to zero. This is only
> telemetry after all.
> 
>  lib/eal/common/eal_common_lcore.c | 33 ++++++++++++++++++++++++++---
>  lib/eal/include/rte_lcore.h       | 35 +++++++++++++++++++++++++++++++
>  lib/eal/version.map               |  1 +
>  3 files changed, 66 insertions(+), 3 deletions(-)
> 
> diff --git a/lib/eal/common/eal_common_lcore.c b/lib/eal/common/eal_common_lcore.c
> index 16548977dce8..210636d21d6b 100644
> --- a/lib/eal/common/eal_common_lcore.c
> +++ b/lib/eal/common/eal_common_lcore.c
> @@ -2,6 +2,7 @@
>   * Copyright(c) 2010-2014 Intel Corporation
>   */
> 
> +#include <inttypes.h>
>  #include <stdlib.h>
>  #include <string.h>
> 
> @@ -422,11 +423,21 @@ rte_lcore_iterate(rte_lcore_iterate_cb cb, void *arg)
>  	return ret;
>  }
> 
> +static rte_lcore_usage_cb lcore_usage_cb;
> +
> +void
> +rte_lcore_register_usage_cb(rte_lcore_usage_cb cb)
> +{
> +	lcore_usage_cb = cb;
> +}
> +

LGTM in general.
One question: I assume this function is supposed to be called just once at app init stage,
so we probably don't need to worry about possible sync issues, right?
If so, then probably worth to mention it in the function formal comments below. 

>  static int
>  lcore_dump_cb(unsigned int lcore_id, void *arg)
>  {
>  	struct rte_config *cfg = rte_eal_get_configuration();
> -	char cpuset[RTE_CPU_AFFINITY_STR_LEN];
> +	char cpuset[RTE_CPU_AFFINITY_STR_LEN], usage_str[256];
> +	struct rte_lcore_usage usage;
> +	rte_lcore_usage_cb usage_cb;
>  	const char *role;
>  	FILE *f = arg;
>  	int ret;
> @@ -446,11 +457,19 @@ lcore_dump_cb(unsigned int lcore_id, void *arg)
>  		break;
>  	}
> 
> +	memset(&usage, 0, sizeof(usage));
> +	usage_str[0] = '\0';
> +	usage_cb = lcore_usage_cb;
> +	if (usage_cb != NULL && usage_cb(lcore_id, &usage) == 0) {
> +		snprintf(usage_str, sizeof(usage_str), ", busy cycles %"PRIu64"/%"PRIu64,
> +			usage.busy_cycles, usage.total_cycles);
> +	}
>  	ret = eal_thread_dump_affinity(&lcore_config[lcore_id].cpuset, cpuset,
>  		sizeof(cpuset));
> -	fprintf(f, "lcore %u, socket %u, role %s, cpuset %s%s\n", lcore_id,
> +	fprintf(f, "lcore %u, socket %u, role %s, cpuset %s%s%s\n", lcore_id,
>  		rte_lcore_to_socket_id(lcore_id), role, cpuset,
> -		ret == 0 ? "" : "...");
> +		ret == 0 ? "" : "...", usage_str);
> +
>  	return 0;
>  }
> 
> @@ -489,7 +508,9 @@ lcore_telemetry_info_cb(unsigned int lcore_id, void *arg)
>  {
>  	struct lcore_telemetry_info *info = arg;
>  	struct rte_config *cfg = rte_eal_get_configuration();
> +	struct rte_lcore_usage usage;
>  	struct rte_tel_data *cpuset;
> +	rte_lcore_usage_cb usage_cb;
>  	const char *role;
>  	unsigned int cpu;
> 
> @@ -522,6 +543,12 @@ lcore_telemetry_info_cb(unsigned int lcore_id, void *arg)
>  		if (CPU_ISSET(cpu, &lcore_config[lcore_id].cpuset))
>  			rte_tel_data_add_array_int(cpuset, cpu);
>  	rte_tel_data_add_dict_container(info->d, "cpuset", cpuset, 0);
> +	memset(&usage, 0, sizeof(usage));
> +	usage_cb = lcore_usage_cb;
> +	if (usage_cb != NULL && usage_cb(lcore_id, &usage) == 0) {
> +		rte_tel_data_add_dict_u64(info->d, "busy_cycles", usage.busy_cycles);
> +		rte_tel_data_add_dict_u64(info->d, "total_cycles", usage.total_cycles);
> +	}
> 
>  	return 0;
>  }
> diff --git a/lib/eal/include/rte_lcore.h b/lib/eal/include/rte_lcore.h
> index 6938c3fd7b81..a92313577355 100644
> --- a/lib/eal/include/rte_lcore.h
> +++ b/lib/eal/include/rte_lcore.h
> @@ -328,6 +328,41 @@ typedef int (*rte_lcore_iterate_cb)(unsigned int lcore_id, void *arg);
>  int
>  rte_lcore_iterate(rte_lcore_iterate_cb cb, void *arg);
> 
> +/**
> + * CPU usage statistics.
> + */
> +struct rte_lcore_usage {
> +	uint64_t total_cycles;
> +	/**< The total amount of time since application start, in TSC cycles. */
> +	uint64_t busy_cycles;
> +	/**< The amount of busy time since application start, in TSC cycles. */
> +};
> +
> +/**
> + * Callback to allow applications to report CPU usage.
> + *
> + * @param [in] lcore_id
> + *   The lcore to consider.
> + * @param [out] usage
> + *   Counters representing this lcore usage. This can never be NULL.
> + * @return
> + *   - 0 if fields in usage were updated successfully. The fields that the
> + *       application does not support should be left to their default value.
> + *   - a negative value if the information is not available or if any error occurred.
> + */
> +typedef int (*rte_lcore_usage_cb)(unsigned int lcore_id, struct rte_lcore_usage *usage);
> +
> +/**
> + * Register a callback from an application to be called in rte_lcore_dump() and
> + * the /eal/lcore/info telemetry endpoint handler. Applications are expected to
> + * report CPU usage statistics via this callback.
> + *
> + * @param cb
> + *   The callback function.
> + */
> +__rte_experimental
> +void rte_lcore_register_usage_cb(rte_lcore_usage_cb cb);
> +
>  /**
>   * List all lcores.
>   *
> diff --git a/lib/eal/version.map b/lib/eal/version.map
> index 7ad12a7dc985..30fd216a12ea 100644
> --- a/lib/eal/version.map
> +++ b/lib/eal/version.map
> @@ -440,6 +440,7 @@ EXPERIMENTAL {
>  	rte_thread_detach;
>  	rte_thread_equal;
>  	rte_thread_join;
> +	rte_lcore_register_usage_cb;
>  };
> 
>  INTERNAL {
> --
> 2.38.1
  
Robin Jarry Jan. 4, 2023, 10:10 a.m. UTC | #3
Konstantin Ananyev, Dec 22, 2022 at 13:41:
> > +static rte_lcore_usage_cb lcore_usage_cb;
> > +
> > +void
> > +rte_lcore_register_usage_cb(rte_lcore_usage_cb cb)
> > +{
> > +	lcore_usage_cb = cb;
> > +}
> > +
>
> LGTM in general.
> One question: I assume this function is supposed to be called just
> once at app init stage, so we probably don't need to worry about
> possible sync issues, right? If so, then probably worth to mention it
> in the function formal comments below.

Yes, this is correct. I'll add a mention in the function docstring to
explain that this should be called once at init. Also I'll add a comment
in the function body to indicate that there is no risk of sync issues.
  
Robin Jarry Jan. 4, 2023, 10:13 a.m. UTC | #4
Morten Brørup, Dec 16, 2022 at 11:47:
> > +	usage_str[0] = '\0';
> > +	usage_cb = lcore_usage_cb;
> > +	if (usage_cb != NULL && usage_cb(lcore_id, &usage) == 0) {
>
> Move memset() inside here, and add comment:
>
> + /* The application's callback may not set all the fields in the structure, so clear it here. */
> + memset(&usage, 0, sizeof(usage));

This may make the code more complex than it needs to be (two nested ifs)
for very little performance benefit. I'm not sure it is worth it. I can
add the comment, though.
  
Robin Jarry Jan. 4, 2023, 10:15 a.m. UTC | #5
Robin Jarry, Dec 16, 2022 at 11:21:
> Allow applications to register a callback that will be invoked in
> rte_lcore_dump() and when requesting lcore info in the telemetry API.
>
> The callback is expected to return the number of TSC cycles that have
> passed since application start and the number of these cycles that were
> spent doing busy work.
>
> Signed-off-by: Robin Jarry <rjarry@redhat.com>
> Acked-by: Morten Brørup <mb@smartsharesystems.com>
> ---
> v4 -> v5:
>
> The callback now takes a pointer to a rte_lcore_usage structure.
> I chose not to include any API version tracking mechanism since the
> unsupported/unused fields can simply be left to zero. This is only
> telemetry after all.

Hi Kevin, Bruce,

did you have a chance to go over this series? Do you have any comments?
  
Morten Brørup Jan. 4, 2023, 10:28 a.m. UTC | #6
> From: Robin Jarry [mailto:rjarry@redhat.com]
> Sent: Wednesday, 4 January 2023 11.14
> 
> Morten Brørup, Dec 16, 2022 at 11:47:
> > > +	usage_str[0] = '\0';
> > > +	usage_cb = lcore_usage_cb;
> > > +	if (usage_cb != NULL && usage_cb(lcore_id, &usage) == 0) {
> >
> > Move memset() inside here, and add comment:
> >
> > + /* The application's callback may not set all the fields in the
> structure, so clear it here. */
> > + memset(&usage, 0, sizeof(usage));
> 
> This may make the code more complex than it needs to be (two nested
> ifs)
> for very little performance benefit. I'm not sure it is worth it. I can
> add the comment, though.

You are right - I missed that. Just adding the comment is fine.


Please also note my comment regarding the rte_lcore_usage_cb() function description:

"should be left to their default value." -> "must not be modified."
  
Konstantin Ananyev Jan. 4, 2023, 10:53 a.m. UTC | #7
> 
> Konstantin Ananyev, Dec 22, 2022 at 13:41:
> > > +static rte_lcore_usage_cb lcore_usage_cb;
> > > +
> > > +void
> > > +rte_lcore_register_usage_cb(rte_lcore_usage_cb cb)
> > > +{
> > > +	lcore_usage_cb = cb;
> > > +}
> > > +
> >
> > LGTM in general.
> > One question: I assume this function is supposed to be called just
> > once at app init stage, so we probably don't need to worry about
> > possible sync issues, right? If so, then probably worth to mention it
> > in the function formal comments below.
> 
> Yes, this is correct. I'll add a mention in the function docstring to
> explain that this should be called once at init. Also I'll add a comment
> in the function body to indicate that there is no risk of sync issues.

Sounds good to me.
Probably we can even print warning or so if some-one tries to overwrite
it once again.
  
Robin Jarry Jan. 18, 2023, 4:46 p.m. UTC | #8
Konstantin Ananyev, Jan 04, 2023 at 11:53:
> Probably we can even print warning or so if some-one tries to overwrite
> it once again.

I'm not sure that is necessary. If an application wants to reset the
callback to NULL at any point in time, I don't see why DPDK should tell
them it is a bad thing.
  
Konstantin Ananyev Feb. 6, 2023, 8:07 p.m. UTC | #9
> 
> Konstantin Ananyev, Jan 04, 2023 at 11:53:
> > Probably we can even print warning or so if some-one tries to overwrite
> > it once again.
> 
> I'm not sure that is necessary. If an application wants to reset the
> callback to NULL at any point in time, I don't see why DPDK should tell
> them it is a bad thing.

Problem is not in resetting cb function itself.
Usually with CB user needs some sort of data structure (to accumulate stats, track states, etc.).
If we allow to reset the CB, then it arises the question when/how should we allow
user to free associated data?
And, as I undersand, we don't have a clear way to do it.
  
Robin Jarry Feb. 6, 2023, 8:29 p.m. UTC | #10
Konstantin Ananyev, Feb 06, 2023 at 21:07:
> Problem is not in resetting cb function itself.
>
> Usually with CB user needs some sort of data structure (to accumulate
> stats, track states, etc.). If we allow to reset the CB, then it
> arises the question when/how should we allow user to free associated
> data?
>
> And, as I undersand, we don't have a clear way to do it.

If the application decides to reset the callback function, they are in
a good position to determine what resources they need to free. I don't
see why EAL should get involved here but I may be missing a point.
  
Konstantin Ananyev Feb. 6, 2023, 8:34 p.m. UTC | #11
> -----Original Message-----
> From: Robin Jarry <rjarry@redhat.com>
> Sent: Monday, February 6, 2023 8:29 PM
> To: Konstantin Ananyev <konstantin.ananyev@huawei.com>; dev@dpdk.org
> Cc: Tyler Retzlaff <roretzla@linux.microsoft.com>; Kevin Laatz <kevin.laatz@intel.com>; Morten Brørup
> <mb@smartsharesystems.com>
> Subject: Re: [PATCH v5 2/4] eal: allow applications to report their cpu usage
> 
> Konstantin Ananyev, Feb 06, 2023 at 21:07:
> > Problem is not in resetting cb function itself.
> >
> > Usually with CB user needs some sort of data structure (to accumulate
> > stats, track states, etc.). If we allow to reset the CB, then it
> > arises the question when/how should we allow user to free associated
> > data?
> >
> > And, as I undersand, we don't have a clear way to do it.
> 
> If the application decides to reset the callback function, they are in
> a good position to determine what resources they need to free.

Yes, app knows what resources it wants to free.
But it has no way to know *when* it is safe to free them.
Just a bit more explanation:
App invokes your function which resets global value of CB.
How would it know that after return from this function none
other thread still not executing this CB right now?
And how determine when this thread will finish with executing CB function?
That's why it might be easier simply not allow to reset it at all....

> I don't
> see why EAL should get involved here but I may be missing a point.
  
Robin Jarry Feb. 6, 2023, 8:39 p.m. UTC | #12
Konstantin Ananyev, Feb 06, 2023 at 21:34:
> Yes, app knows what resources it wants to free.
> But it has no way to know *when* it is safe to free them.
> Just a bit more explanation:
> App invokes your function which resets global value of CB.
> How would it know that after return from this function none
> other thread still not executing this CB right now?
> And how determine when this thread will finish with executing CB function?
> That's why it might be easier simply not allow to reset it at all....

Ok I see. But what should we do to prevent this? Simply ignore the
request and log a warning?
  
Konstantin Ananyev Feb. 6, 2023, 8:44 p.m. UTC | #13
> Konstantin Ananyev, Feb 06, 2023 at 21:34:
> > Yes, app knows what resources it wants to free.
> > But it has no way to know *when* it is safe to free them.
> > Just a bit more explanation:
> > App invokes your function which resets global value of CB.
> > How would it know that after return from this function none
> > other thread still not executing this CB right now?
> > And how determine when this thread will finish with executing CB function?
> > That's why it might be easier simply not allow to reset it at all....
> 
> Ok I see. But what should we do to prevent this? Simply ignore the
> request and log a warning?

That's seems like simplest choice to me...
Or if you still prefer to allow it - put a special comment that it is user
responsibility to handle such possible race-condition (inside his CB function or so).
  
Robin Jarry Feb. 6, 2023, 8:55 p.m. UTC | #14
Konstantin Ananyev, Feb 06, 2023 at 21:44:
> > Ok I see. But what should we do to prevent this? Simply ignore the
> > request and log a warning?
>
> That's seems like simplest choice to me... Or if you still prefer to
> allow it - put a special comment that it is user responsibility to
> handle such possible race-condition (inside his CB function or so).

The issue is that a warning can be easily overlooked and the application
may assume that they can free up resources whereas the callback was
never reset and may still access them.

I wonder if this could be enforced with RTE_BUILD_BUG_ON somehow. Or at
least by checking that the cb value is not NULL with RTE_ASSERT?
  
Konstantin Ananyev Feb. 7, 2023, 1:12 p.m. UTC | #15
> > > Ok I see. But what should we do to prevent this? Simply ignore the
> > > request and log a warning?
> >
> > That's seems like simplest choice to me... Or if you still prefer to
> > allow it - put a special comment that it is user responsibility to
> > handle such possible race-condition (inside his CB function or so).
> 
> The issue is that a warning can be easily overlooked and the application
> may assume that they can free up resources whereas the callback was
> never reset and may still access them.
 
Yes, could happen, in principle. 
 
> I wonder if this could be enforced with RTE_BUILD_BUG_ON somehow.
 
I don't think it is possible, as the value can change at runtime.

> Or at   least by checking that the cb value is not NULL with RTE_ASSERT?

assert() would work, but general rule of thumb - don't panic() in library
functions, but return an error. 
After another thought - might be allowing user to call it at run-time 
and just documenting it properly is enough.
After all there are few ways to overcome the problem in user CB function itself. 
Konstantin
  

Patch

diff --git a/lib/eal/common/eal_common_lcore.c b/lib/eal/common/eal_common_lcore.c
index 16548977dce8..210636d21d6b 100644
--- a/lib/eal/common/eal_common_lcore.c
+++ b/lib/eal/common/eal_common_lcore.c
@@ -2,6 +2,7 @@ 
  * Copyright(c) 2010-2014 Intel Corporation
  */
 
+#include <inttypes.h>
 #include <stdlib.h>
 #include <string.h>
 
@@ -422,11 +423,21 @@  rte_lcore_iterate(rte_lcore_iterate_cb cb, void *arg)
 	return ret;
 }
 
+static rte_lcore_usage_cb lcore_usage_cb;
+
+void
+rte_lcore_register_usage_cb(rte_lcore_usage_cb cb)
+{
+	lcore_usage_cb = cb;
+}
+
 static int
 lcore_dump_cb(unsigned int lcore_id, void *arg)
 {
 	struct rte_config *cfg = rte_eal_get_configuration();
-	char cpuset[RTE_CPU_AFFINITY_STR_LEN];
+	char cpuset[RTE_CPU_AFFINITY_STR_LEN], usage_str[256];
+	struct rte_lcore_usage usage;
+	rte_lcore_usage_cb usage_cb;
 	const char *role;
 	FILE *f = arg;
 	int ret;
@@ -446,11 +457,19 @@  lcore_dump_cb(unsigned int lcore_id, void *arg)
 		break;
 	}
 
+	memset(&usage, 0, sizeof(usage));
+	usage_str[0] = '\0';
+	usage_cb = lcore_usage_cb;
+	if (usage_cb != NULL && usage_cb(lcore_id, &usage) == 0) {
+		snprintf(usage_str, sizeof(usage_str), ", busy cycles %"PRIu64"/%"PRIu64,
+			usage.busy_cycles, usage.total_cycles);
+	}
 	ret = eal_thread_dump_affinity(&lcore_config[lcore_id].cpuset, cpuset,
 		sizeof(cpuset));
-	fprintf(f, "lcore %u, socket %u, role %s, cpuset %s%s\n", lcore_id,
+	fprintf(f, "lcore %u, socket %u, role %s, cpuset %s%s%s\n", lcore_id,
 		rte_lcore_to_socket_id(lcore_id), role, cpuset,
-		ret == 0 ? "" : "...");
+		ret == 0 ? "" : "...", usage_str);
+
 	return 0;
 }
 
@@ -489,7 +508,9 @@  lcore_telemetry_info_cb(unsigned int lcore_id, void *arg)
 {
 	struct lcore_telemetry_info *info = arg;
 	struct rte_config *cfg = rte_eal_get_configuration();
+	struct rte_lcore_usage usage;
 	struct rte_tel_data *cpuset;
+	rte_lcore_usage_cb usage_cb;
 	const char *role;
 	unsigned int cpu;
 
@@ -522,6 +543,12 @@  lcore_telemetry_info_cb(unsigned int lcore_id, void *arg)
 		if (CPU_ISSET(cpu, &lcore_config[lcore_id].cpuset))
 			rte_tel_data_add_array_int(cpuset, cpu);
 	rte_tel_data_add_dict_container(info->d, "cpuset", cpuset, 0);
+	memset(&usage, 0, sizeof(usage));
+	usage_cb = lcore_usage_cb;
+	if (usage_cb != NULL && usage_cb(lcore_id, &usage) == 0) {
+		rte_tel_data_add_dict_u64(info->d, "busy_cycles", usage.busy_cycles);
+		rte_tel_data_add_dict_u64(info->d, "total_cycles", usage.total_cycles);
+	}
 
 	return 0;
 }
diff --git a/lib/eal/include/rte_lcore.h b/lib/eal/include/rte_lcore.h
index 6938c3fd7b81..a92313577355 100644
--- a/lib/eal/include/rte_lcore.h
+++ b/lib/eal/include/rte_lcore.h
@@ -328,6 +328,41 @@  typedef int (*rte_lcore_iterate_cb)(unsigned int lcore_id, void *arg);
 int
 rte_lcore_iterate(rte_lcore_iterate_cb cb, void *arg);
 
+/**
+ * CPU usage statistics.
+ */
+struct rte_lcore_usage {
+	uint64_t total_cycles;
+	/**< The total amount of time since application start, in TSC cycles. */
+	uint64_t busy_cycles;
+	/**< The amount of busy time since application start, in TSC cycles. */
+};
+
+/**
+ * Callback to allow applications to report CPU usage.
+ *
+ * @param [in] lcore_id
+ *   The lcore to consider.
+ * @param [out] usage
+ *   Counters representing this lcore usage. This can never be NULL.
+ * @return
+ *   - 0 if fields in usage were updated successfully. The fields that the
+ *       application does not support should be left to their default value.
+ *   - a negative value if the information is not available or if any error occurred.
+ */
+typedef int (*rte_lcore_usage_cb)(unsigned int lcore_id, struct rte_lcore_usage *usage);
+
+/**
+ * Register a callback from an application to be called in rte_lcore_dump() and
+ * the /eal/lcore/info telemetry endpoint handler. Applications are expected to
+ * report CPU usage statistics via this callback.
+ *
+ * @param cb
+ *   The callback function.
+ */
+__rte_experimental
+void rte_lcore_register_usage_cb(rte_lcore_usage_cb cb);
+
 /**
  * List all lcores.
  *
diff --git a/lib/eal/version.map b/lib/eal/version.map
index 7ad12a7dc985..30fd216a12ea 100644
--- a/lib/eal/version.map
+++ b/lib/eal/version.map
@@ -440,6 +440,7 @@  EXPERIMENTAL {
 	rte_thread_detach;
 	rte_thread_equal;
 	rte_thread_join;
+	rte_lcore_register_usage_cb;
 };
 
 INTERNAL {