MadsBoydMadsen

MadsBoydMadsen

Macro to provide timing for a function call - handle any signature

I’m working on a macro to support automatic timing for functions.
If I have this - say:

def double(x) do
  x + x
end

I would like to be able to change it to this on a needs basis:

def_timed double(x) do
  x + x
end

I’ve got the basics of a macro written that can handle this:

defmodule Cw.Utilities.Timing.DefTimed do
  defmacro def_timed({name, _meta, args} = _ast, do: body) do
    arg_vars = Enum.map(args, fn {arg_name, _, _} -> arg_name end)

    quote do
      def unquote(name)(unquote_splicing(args)) do
        Timer.wrap_timed(
          fn unquote_splicing(arg_vars) -> unquote(body) end,
          [unquote_splicing(arg_vars)],
          unquote(name)
        )
      end
    end
  end
end

defmodule Timer do
  def wrap_timed(fun, args, name) do
    start_time = System.monotonic_time(:millisecond)
    result = apply(fun, args)
    end_time = System.monotonic_time(:millisecond)

    IO.puts("""
    [TIMED] Function: #{name}
    [TIMED] Arguments: #{inspect(args)}
    [TIMED] Execution time: #{end_time - start_time} ms
    """)

    result
  end
end

This works fine for functions with simple signatures as above.
But it doesn’t work when the signatures includes keyword-list, optionals, guards or pattern-matching.

I sense, that this must be a solved problem, but I cannot find anything online to demonstrate how to do it.
I’m looking for help to work it out.

NOTE: One of my colleagues have just pointed out, that macros should only be used sparingly (Meta-programming anti-patterns — Elixir v1.20.2).
My use-case strikes me as being pretty perfect for a macro, as adding and removing instrumentation would be a breeze (good luck to me :smiley: )
Is there a better - and equally straight forward - way for me to achieve what I’m trying to do ?

Most Liked

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Checkout Elixir function decorators — decorator v1.4.0 which implements a module attribute based decorator pattern.

The main code for defining decorators is here: decorator/lib/decorator/decorate.ex at master · arjan/decorator · GitHub

billylanchantin

billylanchantin

I’m honestly don’t think this is a solved problem largely for the reasons you note: the forms the AST can take are quite varied. While Elixir does give you the ability to manipulate AST directly, it doesn’t give you many nice helpers like Macro.find_the_do_block_in_this_ast/1.

(I’m speculating here, but I imagine it’s because the AST is an implementation detail subject to change.)

My personal experience with writing macros is that you’re on your own somewhat. The Meta-Programming chapters of the docs are quite good. But for specific applications, you often need to dig in and see what the expressions you plan to work with happen to come out as in AST form.

Note: another snag you’ll hit is that there are other blocks that come after the do block:

def reciprocal(x) when is_integer(x) do
  1 / x
rescue
  ArithmeticError -> :infinity
end
al2o3cr

al2o3cr

The tricky part is that a function doesn’t even need to name its arguments if it pattern-matches on them, for instance:

def foo([a | _], %{wat: b}) do
  ...
end

Replacing def with def_timed:

def_timed foo([a | _], %{wat: b}) do
  ...
end

could expand to something like:

def foo(arg1, arg2) do
  Timer.wrap_timed(
    &untimed_foo/2,
    [arg1, arg2],
    :foo
  )
end

def untimed_foo([a | _], %{wat: b}) do
  ...
end

untimed_foo would use the original args AST unaltered, while the code generated in foo only cares about length(args).

Supporting default args directly in def_timed would be a lot of extra hassle, but using a do-less def would let you avoid that:

def timed_thing_with_defaults(x, y \\ 1, z \\ 2)
def_timed timed_thing_with_defaults(x, y, z) do
  ...
end

Last Post!

dimitarvp

dimitarvp

Then here’s one more homework I’m afraid: you’ll need to integrate OpenTelemetry in your project.

Where Next?

Popular in Questions Top

jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New

Other popular topics Top

baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New

We're in Beta

About us Mission Statement