Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Zephyr instrumentation subsystem #57371

Closed
wants to merge 0 commits into from

Conversation

gromero
Copy link
Collaborator

@gromero gromero commented Apr 28, 2023

Hello Zephyr community!

This is the PoC / initial implementation for the proposed Zephyr instrumentation subsystem. It leverages the compiler function instrumentation to collect tracing and profiling information in Zephyr RTOS.

This has been tested against the ST b_u585i_iot02a board. Please note that, since it relies on the retained_mem driver, if another board is used for testing the proper device-tree overlay for the target board needs to be added to boards/ dir accordingly.

Please see samples/subsys/instrumentation/src/main.c sample for details on how to experiment with it.

RFC: #57373

Thanks!

Cheers,
Gustavo & Kevin

Copy link
Collaborator

@nordicjm nordicjm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments on retained memory usage - would suggest using retention subsystem.


*/

const static struct device *retained_mem_dev = DEVICE_DT_GET(DT_ALIAS(retainedmemdevice));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nordicjm chosen in this example is select boot mode, which is not really necessary, but I'm not sure I'm following you here. Could you please expand on it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a board dts or an overlay, you could have something like this:

	retainedmem {
		compatible = "zephyr,retained-ram";
		status = "okay";
		#address-cells = <1>;
		#size-cells = <1>;

		instru0: instru@0 {
			compatible = "zephyr,retention";
			status = "okay";
			reg = <0x0 0x100>;
			prefix = [08 04];
			checksum = <1>;
		};
	};

	chosen {
		zephyr,instrumentation-area = &instru0;
	};

Then that way you can access it by using static const struct device *retained_mem_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_instrumentation_area));
This is not a required change.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, got it. Thanks for the explanation!

subsys/instrumentation/common/instr_common.c Outdated Show resolved Hide resolved
void* stopper_callee;

/*
* TODO(gromero): remove hack and use new RETAINED_MEM_MUTEXES=n config.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise retention subsystem has a similar Kconfig, note that both retained memory and retention subsystem mutex Kconfigs would need to be set to n

Copy link
Collaborator Author

@gromero gromero Jun 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nordicjm Hi. I was not able to find any Kconfig syntax that would allow forcing RETAINED_MEM_MUTEXES=n
and RETENTION_MUTEXES=n from subsys/instrumentation/Kconfig. I put them in prj.conf (which works, but is annoying for the user that would need to know about it and set it per project). Any idea on how to set them in Kconfig?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it is not possible to de-select a Kconfig from a Kconfig, I will need to rework how it is done

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have posted this: #59254 will have a look at it after the 3.4 release is complete

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nordicjm Hi Jamie. Thank you!

Copy link
Collaborator

@teburd teburd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A first pass look see and spotted some things, will wait for the non-draft to look harder

subsys/instrumentation/timestamp/timestamp.c Outdated Show resolved Hide resolved
}

__no_instrumentation__
void instr_event_handler(enum instr_event_types type, void *callee, void *caller)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to account for SMP in any way. Presumably it would be some metadata in the event or an entirely different stream (probably preferable to avoid locking) per core.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@teburd Right, it doesn't currently account for SMP. But indeed, what I had in mind was to use one stream per CPU to avoid locking/lock contention issues.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only downside of this is that output streams need to be muxed in some way then, and will have some lock likely around it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@teburd Not sure if I understood your last comment correctly. From your first comment I understood that to avoid locking issues you're suggesting one buffer -- so one stream -- per CPU (you said "core" but since a core may have multiple CPUs, I assumed it would be actually one buffer/stream per CPU to avoid the locking, because each CPU will execute, for instance, different threads, hence different code and stack to trace/profile) -- which I agreed. I'm confused about which approach you referred to as "this" in your last comment tho :-)

@carlocaione carlocaione self-requested a review April 29, 2023 09:02
Copy link
Collaborator

@carlocaione carlocaione left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll need to come back later to this

include/zephyr/instrumentation/instrumentation.h Outdated Show resolved Hide resolved
subsys/instrumentation/common/instr_common.c Outdated Show resolved Hide resolved
@nashif nashif assigned nashif and unassigned tejlmand May 1, 2023
@gromero
Copy link
Collaborator Author

gromero commented May 2, 2023

@nordicjm @teburd @carlocaione @nashif Thanks a lot for the first pass and for the valuable suggestions! I'm working on getting the CI green now, but I'm also going on vacation today, so bear with me. I'll address your comments next :-)

@teburd
Copy link
Collaborator

teburd commented May 3, 2023

Another question that came to mind, could this work with a sampling profiler, e.g. ITM periodically sampling of the program counter?

@microbuilder
Copy link
Member

Another question that came to mind, could this work with a sampling profiler, e.g. ITM periodically sampling of the program counter?

The underlying polling mechanism would be quite different than the compiler instrumentation this is based on, where entry/exit hooks are inserted at compile time. The subsystem could certainly be updated to accommodate that in a second stage, using the underlying buffer processing tools, transports, etc., but I think doing one job well with the compiler-generated entry/exit hooks first would be more efficient to start.

@carlescufi
Copy link
Member

carlescufi commented May 23, 2023

Architecture WG:

  • @gromero notes that this subsystem allows us to instrument all the code without having to modify the code at all
  • @nashif states that the tracing subsystem is very selective, it requires macros (SYS_PORT_TRACING_) to be inserted in the code. For general developer use this proposal is very useful to enable profiling of the application, the overhead is added per-function (including internal and external APIs)
  • @microbuilder explains that the instrumentation can be limited to a subset of the code by filtering using exclude filters that work on regexes
  • @gromero clarifies that the filtering works at compile-time, whereas the trigger/stop functions work at runtime. Both approaches can be combined for fine-grained filtering. He also mentions future improvements where live patching could be used or more refined selection of the functions
  • gcc has an exclude list that works for both object files and filenames
  • @carlescufi asks why we cannot use the same transport for both tracing, logging, coredump and now instrumentation
  • @nashif agrees that we should coalesce transport backends in the future
  • @gromero mentions that SMP is not supported, @nashif says tracing doesn't support it either

samples/subsys/instrumentation/src/main.c Outdated Show resolved Hide resolved
samples/subsys/instrumentation/src/main.c Outdated Show resolved Hide resolved
@gromero gromero force-pushed the instr_subsystem branch 2 times, most recently from ddceaed to aecf328 Compare June 14, 2023 01:50
@gromero
Copy link
Collaborator Author

gromero commented Jul 25, 2023

@gromero Is it me or we've lost documentation in the latest update ?

@erwango Hi Erwan! The only existing "docs" so far are the one from the RFC itself, the one in samples, and the function comments in the .h files.

@erwango
Copy link
Member

erwango commented Jul 25, 2023

@erwango Hi Erwan! The only existing "docs" so far are the one from the #57373 itself, the one in samples, and the function comments in the .h files.

Oh, my bad I was looking for a README... that's ok, thanks!

include/zephyr/instrumentation/instrumentation.h Outdated Show resolved Hide resolved

#include <zephyr/instrumentation/instrumentation.h>

#define DEBUG 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think this should be here


void instr_rb_init(void);

struct instr_record *instr_rb_put_item_claim(enum instr_event_types type);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs documentation

struct instr_record *instr_rb_get_item(struct instr_record *record);
int instr_rb_get_item_wrapping(struct instr_record *record);

/* TODO(gromero): get_item_size() probably not for external use. */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO needs to be done

subsys/instrumentation/Kconfig Outdated Show resolved Hide resolved
default 256
range 1 4096
help
Maximum number of functions to collect statistics. Set the maximum
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thanks.

*
*/

const struct device *retention0 = DEVICE_DT_GET(DT_NODELABEL(retention0));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retention works in uninitialised RAM already, if it needs additional code to work across another core than it can be added as a new retention driver, that's not a problem

#endif

/* See instr_fundamentals_initialized() */
uint16_t magic = 0xABBA;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would want to be static

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, fixed! Thanks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, that's not right. Making it static defeats the purpose of 'magic'. static variables are initialized before program execution. What is needed here is a dynamic variable, which will be initialized at runtime, this way it's possible to use that variable (magic) to verify that all other important dynamic variables are properly initialized too. I'm reverting this change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The likelyhood of another variable in a program or library being just called "magic" seems fairly high, so by not making this static you're creating a high chance of variable name collision.

subsys/instrumentation/common/instr_common.c Outdated Show resolved Hide resolved
head = 0;

/* Second, write remaining item bytes not written yet. */
second_chunk = instr_rb_get_item_size(rec->header.type) - first_chunk;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason the zephyr ring buffer isn't used?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nordicjm The reason is that we planned at the beginning to use certain fields in the packet to inform variable length records allowing "maximum compression" when transferring the records from the target to the host. But the data stream is decoded in the host using lib babeltrace. However, we found out that it currently only supports Common Trace Format v1.8, which does not support decoding variable length data based on a length field (only for char[] iirc). Hence I ended up using fixed-length records, which now can be used with the Zephyr ring buffer API (which only supports fixed-length items). I'm investigating now if I can replace it with Zephyr ring buffer API to simply the PR for the moment.

@github-actions
Copy link

github-actions bot commented Oct 6, 2023

This pull request has been marked as stale because it has been open (more than) 60 days with no activity. Remove the stale label or add a comment saying that you would like to have the label removed otherwise this pull request will automatically be closed in 14 days. Note, that you can always re-open a closed pull request at any time.

@github-actions github-actions bot added Stale and removed Stale labels Oct 6, 2023
@gromero
Copy link
Collaborator Author

gromero commented Nov 5, 2023

I just would like to say I'm finding some cycles to address the latest comments on this PR this week.

@teburd
Copy link
Collaborator

teburd commented Dec 6, 2023

Cool, would be great to see this and improvements on it in the tree

@gromero
Copy link
Collaborator Author

gromero commented Dec 6, 2023

I've updated the PR addressing most of the @nordicjm comments. While I wait for the CI to get green I'm investigating if I can replace my variable-length ring buffer API with Zephyr's ring buffer API to simply the PR for the moment.

@gromero gromero force-pushed the instr_subsystem branch 2 times, most recently from 9e2142c to 5f573cc Compare December 8, 2023 22:29
@gromero
Copy link
Collaborator Author

gromero commented Dec 8, 2023

Blocked by issue #66334

@microbuilder
Copy link
Member

Blocked by issue #66334

This is merged in, so hopefully unblocks progress here?

@gromero
Copy link
Collaborator Author

gromero commented Jan 18, 2024

Blocked by issue #66334

This is merged in, so hopefully unblocks progress here?

@microbuilder Yep, thanks!

@Ablu
Copy link

Ablu commented Mar 13, 2024

I was able to get this working in a virtualized environment (mps2_an385).

Steps:

  1. checked out this PR
  2. cherry-pick 6407120 (merged already, but not part of this PR)
  3. allow exposing the serial I/O over socket:
diff --git a/cmake/emu/qemu.cmake b/cmake/emu/qemu.cmake
index 6103fc1cef..6dd1686f0d 100644
--- a/cmake/emu/qemu.cmake
+++ b/cmake/emu/qemu.cmake
@@ -63,14 +63,17 @@ if(QEMU_PTY)
 elseif(QEMU_PIPE)
   # Redirect console to a pipe, used for running automated tests.
   list(APPEND QEMU_FLAGS -chardev pipe,id=con,mux=on,path=${QEMU_PIPE})
   # Create the pipe file before passing the path to QEMU.
   foreach(target ${qemu_targets})
     list(APPEND PRE_QEMU_COMMANDS_FOR_${target} COMMAND ${CMAKE_COMMAND} -E touch ${QEMU_PIPE})
   endforeach()
+elseif(QEMU_SOCKET)
+  # Serve console I/O on a port.
+  list(APPEND QEMU_FLAGS -chardev socket,id=con,mux=on,server=on,host=127.0.0.1,port=4321)
 else()
   # Redirect console to stdio, used for manual debugging.
   list(APPEND QEMU_FLAGS -chardev stdio,id=con,mux=on)
 endif()
 
 # Connect main serial port to the console chardev.
 list(APPEND QEMU_FLAGS -serial chardev:con)
  1. Tweak zara.py to allow using pySerial's support for alternative serial backends:
diff --git a/scripts/zaru.py b/scripts/zaru.py
index a99d85c87a..107b5874b7 100755
--- a/scripts/zaru.py
+++ b/scripts/zaru.py
@@ -206,27 +206,27 @@ def connect_to_target(port, verbose=False):
 
     Connect to target using serial device 'port'. A ping command is sent
     to the target to check if it's alive and responding. If target responds
     'True' is return, otherwise 'False' is returned.
     """
 
     try:
-        sport = serial.Serial(port, baudrate=115200, timeout=0.2, write_timeout=1)
+        sport = serial.serial_for_url(port, baudrate=115200, timeout=0.2, write_timeout=1)
         if verbose:
             print(f"Using '{port}' to connect.")
 
         if ping(sport):
             if verbose:
                 print("Connected to target.")
             return sport
         else:
             print("No response from target. Is the correct FW flashed? Also, check if port is open by another program.")
             sys.exit(1)
-    except serial.SerialException:
-        print(f"Could not open serial device '{port}'.")
+    except serial.SerialException as e:
+        print(f"Could not open serial device '{port}': {e}")
         sys.exit(1)
 
 
 def reboot_target(port, verbose=False):
     """Reboot target.
 
     Reboot target connected to the port 'port'. After the reboot the target is
  1. Run the sample: west build -b mps2_an385 samples/subsys/instrumentation/ -DQEMU_SOCKET=y -t run
  2. Perform tracing:
./scripts/zaru.py  --serial='socket://localhost:4321' trace -v -c get_sem_and_exec_function
./scripts/zaru.py  --serial='socket://localhost:4321' reboot
./scripts/zaru.py  --serial='socket://localhost:4321' trace -v --perfetto --output /tmp/perfetto_zephyr.json
  1. Open /tmp/perfetto_zephyr.json in Perfetto UI.

There are existing knobs to tie the serial to a /dev/pts or pipes. Unfortunately neither are supported by pySerial. Socket seems to be easy enough, but comes with the usual port allocation issues of course. Overall, utilizing pySerial's URI scheme looks fairly flexible to me... But I have no tested this on actual hardware where one might need to tweak baud rate or other settings.

@microbuilder
Copy link
Member

  •    sport = serial.serial_for_url(port, baudrate=115200, timeout=0.2, write_timeout=1)
    
./scripts/zaru.py  --serial='socket://localhost:4321' trace -v -c get_sem_and_exec_function
./scripts/zaru.py  --serial='socket://localhost:4321' reboot
./scripts/zaru.py  --serial='socket://localhost:4321' trace -v --perfetto --output /tmp/perfetto_zephyr.json

We should get that into any eventual documentation as an example.

@gromero
Copy link
Collaborator Author

gromero commented Mar 14, 2024

@carlocaione @evgeniy-paltsev cc @nashif @nordicjm @teburd @erwango

Hi! Folks, this PR is done for another review in my view. @carlocaione Erik (@Ablu) addressed your comment on allowing the instrumentation to run on a VM, so you don't have to touch any cable to try it -- see the example in Erik's comment above. I've rebased on main and updated the branch with his changes. So now it's trivial to experiment with the instrumentation using the mps2_an385 board (QEMU VM).

@evgeniy-paltsev, your comments were addressed but are still blocking the PR. Could you please have another look?

In general, how can we move on and get this PR merged? Over the months since my presentation in Prague a handful of people showed interest in this work and also expressed that it's a reasonable approach. Is there anything controversial that needs more discussion?

@gromero
Copy link
Collaborator Author

gromero commented Mar 14, 2024

The CI errors seem not related to the changes in this PR. Should I re-trigger?

@gromero
Copy link
Collaborator Author

gromero commented Mar 14, 2024

I accidentally closed this RFC PR when trying to understand the CI errors. But no worries, I think it's time to remove the RFC status and update the description text since the code was tested on new boards since its first version and after several review rounds. Please check PR: #70253

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.