henrique-marcomini-m

henrique-marcomini-m

Is using apply a good way to approach dinamic calls?

TL;DR

Is there a more performant and clean way to call methods when I only know which methods (and modules) I will call after a response from the database


This question uses phoenix but is not about phoenix

I have a endpoint (lets call it legacy_reports) that return report files metadata to the user,like name, size, and download link.

My original problem was that when this endpoint was made, it was made for a specif report generator and to refactor everything in the back and in the front would take about 3 or 5 days. So I tweaked a bit the controller and the view of legacy_reports and added the new report to it.

To not suffer from the same problem in the future, I implemented a behaviour to extract the data from the report, and added the following code to the view.

def render("index.json", %{legacy_reports: leagacy_reports, new_reports: new_reports}) do
    %{
      legacy_reports:
        render_many(legacy_reports, LegacyReportView, "legacy_report.json", as: :legacy_report) ++
          render_many(new_reports, LegacyReportView, "new_report.json", as: :new_report)
    }
  end

  def render("show.json", %{legacy_report:legacy_report}) do
    %{legacy_report: render_one(legacy_report, LegacyReportView, "Legact_report.json")}
  end

  def render("legacy_report.json", %{new_report: new_report}) do

    module_atom =
      case new_report.service do
        "SomeService" -> :"Elixir.MyApp.Reports.SomeService"
        _ -> nil
      end

    %{
      some_key: apply(module_atom, :extract_some_key, [new_report]),
      ...
    }
  end

  def render("legacy_report.json", %{legacy_report: legacy_report}) do
    {start_date, end_date} = decode_report_info(legacy_report.report_info)

    %{
      some_key: legacy_report.some_key,
     ...
    }
  end

...

But since this a dynamic way to use apply, I’m afraid it will have some performance drawbacks. Is there a better way to do it without using a whole lot of cases?

Marked As Solved

OvermindDL1

OvermindDL1

Here, a quick benchmark that I used to test various forms of direct and indirect calls that I made in the past that I just ran again on the latest elixir/beam:

╰─➤  mix bench module_calls

Benchmarking Classifier: simple
===============================

Operating System: Linux"
CPU Information: AMD Phenom(tm) II X6 1090T Processor
Number of Available Cores: 6
Available memory: 15.67 GB
Elixir 1.8.1
Erlang 21.2.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
parallel: 1
inputs: types
Estimated total run time: 42 s


Benchmarking Anon-Closure with input types...
Benchmarking Anon-Empty with input types...
Benchmarking Anon-Module with input types...
Benchmarking Direct with input types...
Benchmarking Direct-Apply with input types...
Benchmarking Indirect with input types...
Benchmarking Indirect-Apply with input types...

##### With input types #####
Name                     ips        average  deviation         median         99th %
Direct               23.04 M      0.0434 μs     ±7.60%      0.0420 μs      0.0620 μs
Direct-Apply         22.99 M      0.0435 μs     ±9.82%      0.0420 μs      0.0520 μs
Anon-Module          15.59 M      0.0641 μs   ±139.50%      0.0600 μs       0.100 μs
Anon-Closure         15.25 M      0.0656 μs   ±211.37%      0.0600 μs       0.100 μs
Anon-Empty           13.71 M      0.0729 μs     ±8.78%      0.0690 μs      0.0920 μs
Indirect-Apply       12.78 M      0.0783 μs     ±6.61%      0.0760 μs      0.0900 μs
Indirect             12.59 M      0.0794 μs     ±8.74%      0.0760 μs       0.100 μs

Comparison: 
Direct               23.04 M
Direct-Apply         22.99 M - 1.00x slower
Anon-Module          15.59 M - 1.48x slower
Anon-Closure         15.25 M - 1.51x slower
Anon-Empty           13.71 M - 1.68x slower
Indirect-Apply       12.78 M - 1.80x slower
Indirect             12.59 M - 1.83x slower

Memory usage statistics:

Name              Memory usage
Direct                   280 B
Direct-Apply             280 B - 1.00x memory usage
Anon-Module              280 B - 1.00x memory usage
Anon-Closure             280 B - 1.00x memory usage
Anon-Empty               280 B - 1.00x memory usage
Indirect-Apply           280 B - 1.00x memory usage
Indirect                 280 B - 1.00x memory usage

**All measurements for memory usage were the same**

A ‘direct’ call is when the module is known at compile-time, an indirect call is when the module is not known at compile-time, an anon-module is something like &Mod.blah/2, an anon-closure is like fn _ -> an_external_var end and an anon-empty is like fn _ -> nil end.

What this says is that both SomeMod.blah(1) and apply(SomeMod, :blah, [1]) have the same cost, because they actually do get lowered down to the same beam instructions. bound_mod.blah(1) and apply(bound_mod, :blah, [1]) are the same speed as well, and still faster than half the speed of a direct call. And anonymous calls are about the same as indirect calls too.

So 2 direct calls is barely slower than a single indirect call, so an indirect call you can consider is about equal to 2 direct calls, which are already way plenty fast. So it’s not something to worry about in other words, feel free to use apply. :slight_smile:

Also Liked

axelson

axelson

Scenic Core Team

I do have a quick note about your code. Since :Elixir.MyApp.Reports.SomeService == MyApp.Reports.SomeService I would change:

    module_atom =
      case new_report.service do
        "SomeService" -> :"Elixir.MyApp.Reports.SomeService"
        _ -> nil
      end

to:

    module_atom =
      case new_report.service do
        "SomeService" -> MyApp.Reports.SomeService
        _ -> nil
      end

Actually I would probably extract that whole snippet to a new private function

henrique-marcomini-m

henrique-marcomini-m

Damn, you rock. Thank you a lot.

Where Next?

Popular in Questions 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
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
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
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
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 55125 245
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement