aya-rs/aya-log

Refactor to do all formatting in userspace

willfindlay opened this issue · 10 comments

Per discussion on Discord, we're interested in refactoring aya-log to something more defmt-like, performing all formatting in userspace rather than using ufmt. I'm filing this as a tracking issue so we can discuss progress.

I looked into this a while back. defmt as is isn't usable for us, it does things like interning and compression that complicate the implementation a lot (it requires a linker script and a special ELF layout). What I think we should do is take the defmt macros (it's already a separate crate), and plug in a different implementation for the Format trait, where we require everything be Pod and we just copy_from_slice() into a buffer we send as a perf event like aya-log already does.

What I think we should do is take the defmt macros (it's already a separate crate), and plug in a different implementation for the Format trait, where we require everything be Pod and we just copy_from_slice() into a buffer we send as a perf event like aya-log already does.

Right, I like this approach of reusing what we can and taking inspiration where necessary.

I think we should make a check list of action items so it's easier to split up the work if needed.

I'm trying to dig into defmt sources right now. And I'm starting to thing that what we might need is a different implementation of Logger:

https://github.com/knurling-rs/defmt/blob/5ef0ba7979e0eb75f981e512c2dd42b7df88fcdf/defmt/src/traits.rs#L61-L130

Since its description mentions that

This trait's methods will be called by the defmt logging macros to transmit the
encoded log data over the wire. The call order is:

  • One acquire() call to start the log frame.
  • Multiple write() calls, with fragments of the log frame data each.
  • One release() call.

The data passed to write() is unencoded. Implementations MUST encode it with Encoder
prior to sending it over the wire. The simplest way is for acquire() to call Encoder::start_frame(),
write() to call Encoder::write(), and release() to call Encoder::end_frame().

There is also a more detailed description of the write method:

https://github.com/knurling-rs/defmt/blob/5ef0ba7979e0eb75f981e512c2dd42b7df88fcdf/defmt/src/traits.rs#L114-L129

We may simply skip encoding in our write implementation. What's more, it seems like defmt provides an option to skip encoding with raw encoding format, according to https://defmt.ferrous-systems.com/encoding.html

The question now is how we could skip interning.

Huh, it seems like interning is also used in defmt-macros

macros/src/construct.rs:29:pub(crate) fn interned_string(string: &str, tag: &str, is_log_statement: bool) -> TokenStream2 {
macros/src/construct/symbol.rs:20:    /// * `defmt_fmt`, `defmt_str` for interned format strings and string literals.
macros/src/derives/format/codegen.rs:31:    let format_tag = construct::interned_string(&format_string, "derived", false);
macros/src/derives/format/codegen/enum_data.rs:14:            format_tag: construct::interned_string("!", "derived", false),
macros/src/derives/format/codegen/enum_data.rs:49:    let format_tag = construct::interned_string(&format_string, "derived", false);
macros/src/function_like/intern.rs:8:    construct::interned_string(&literal.value(), "str", false).into()
macros/src/function_like/log.rs:39:    let header = construct::interned_string(&format_string, level.as_str(), true);
macros/src/function_like/println.rs:33:    let header = construct::interned_string(&format_string, "println", true);
macros/src/function_like/write.rs:37:    let format_tag = construct::interned_string(&format_string, "write", false);
macros/src/items/bitflags.rs:25:    let format_tag = construct::interned_string(&format_string, "bitflags", false);

So I'm wondering if it even makes sense to reuse anything from defmt. Maybe we should rather do everything in aya-log and just:

  • remove ufmt from aya-log-ebpf, require all supported types to be a Pod and send them into a buffer with copy_from_slice with currently existing macros
  • use any kind of formatter in userspace

So I'm wondering if it even makes sense to reuse anything from defmt. Maybe we should rather do everything in aya-log and just:

* remove ufmt from aya-log-ebpf, require all supported types to be a Pod and send them into a buffer with `copy_from_slice` with currently existing macros

* use any kind of formatter in userspace

You'll find out that in order to do that, you need large chunks of defmt-macros (minus the interning) :P

defmt-macros defines a formatting DSL to specify types and display hints, see https://defmt.ferrous-systems.com/macros.html. You'll need something like that in order to know how to encode, decode and format the bytes you send over perf buffers.

To expand a bit since I was a bit dense: when you log something with defmt, it "sends over" [format string][arguments referenced by the format string]. Then the process that does the formatting (user space in our case), reads the format string, and based on the content (the types and display hints) of the format string it decodes and format the [arguments referenced by the format string]. That's the part that i think is reusable, together with the proc macros for info!() error!() etc.

I'm sorry, but I still can't see how defmt-macros can be reused minus the interning, if you mean using it as an actual crate and importing the macros as they are, it seems to be impossible for me.

For example, with the info!() macro we start here:

https://github.com/knurling-rs/defmt/blob/5ef0ba7979e0eb75f981e512c2dd42b7df88fcdf/macros/src/lib.rs#L136-L140

Then we go to function_like::log::expand, so to those two functions:

https://github.com/knurling-rs/defmt/blob/5ef0ba7979e0eb75f981e512c2dd42b7df88fcdf/macros/src/function_like/log.rs#L17-L63

Here is a line which tells me that it depends on interned strings:

https://github.com/knurling-rs/defmt/blob/5ef0ba7979e0eb75f981e512c2dd42b7df88fcdf/macros/src/function_like/log.rs#L39

And the definition (which for me seems to try to get the string from the DWARF):

https://github.com/knurling-rs/defmt/blob/5ef0ba7979e0eb75f981e512c2dd42b7df88fcdf/macros/src/construct.rs#L29-L52

So I think we will need to modify them to our needs.

Oh sorry I realize I could have been clearer. I meant forking/copying the macro code from defmt-macros, not using it as is

So, to sum it up, what most likely needs to be done is:

  • Figuring out what part of code we need from defmt-macros
  • just implement it in aya-log 🤷‍♂️

or...

  • implementing Logger trait to send data to the ring buffers (for now I started doing that)
  • fork the main defmt repo (so far I'm doing that)
    • defmt-macros is the most important part to reuse, but...
    • it relies on interned strings, we have to make that optional
    • we might also need to modify/implement the Format trait? not sure about that though
  • implement the userspace consumer

Leaning towards the 1nd option so far.

Tricky problems to solve:

  • Our Formatter has to output the data to AYA_LOGS map. It must be done with AYA_LOGS.output(&ctx, &buf, 0). But how do we pass ctx to Formatter? We cannot pass it to the write(bytes: &[u8]) method of Formatter trait.

@willfindlay let me know if you would like to work on any of the items, if you see any more work items or if you see anything wrong with what I wrote here (which is very likely, honestly this is how I still feel like when writing this).