dmolina / DaemonMode.jl

Client-Daemon workflow to run faster scripts in Julia
MIT License
272 stars 16 forks source link
hacktoberfest

DaemonMode

Build Status with Github Action Documentation DOI

Introduction

Julia is a great language, but the Just-in-Time compiler implies that loading a package could takes a considerable time, this is called the first plot problem.

It is true that this time is only required for the first time (and there are options, like using and the package Revise). However, it is a great disadvantage when we want to use Julia to create small scripts.

This package solves that problem. Inspired by the daemon-mode of Emacs, this package uses a server/client model. This allows julia to run scripts a lot faster, because the package is maintained in memory between the runs of (to run the same script several times).

Introduced in JuliaCon 2020

This package has been mentioned in JuliaCon 2020, Thank you, Fredrik Ekre!

DaemonMode in JuliaCon

Presented in JuliaCon 2021

I gave a talk Faster scripts in Julia with DaemonMode.jl about the package and its advantages in the JuliaCon 2021 in the Talk in JuliaCon 2021. Now it is online at: Faster scripts in Julia with DaemonMode.jl.

Usage

Process

The process is the following:

  1. The client process sends the program program.jl with the required arguments to the server.

  2. The server receives the program name, and run it, returning the output to the client process.

  3. The client process receives the output and shows it to the console.

Example

Suppose that we have the script test.jl

using CSV, DataFrames

fname = only(ARGS)
df = CSV.File(fname) |> DataFrame
println(first(df, 3))

The normal method is:

$ time julia test.jl tsp_50.csv
...
3×2 DataFrame
│ Row │ x        │ y          │
│     │ Float64  │ Float64    │
├─────┼──────────┼────────────┤
│ 1   │ 0.420169 │ 0.628786   │
│ 2   │ 0.892219 │ 0.673288   │
│ 3   │ 0.530688 │ 0.00151249 │

real    0m18.831s
user    0m18.670s
sys     0m0.476s

Only loading the CSV, DataFrames, and reading a simple file takes 18 seconds on my computer. Every time that you run the program is going to take these 18 seconds.

using DaemonMode:

$ julia --startup-file=no -e 'using DaemonMode; serve()' &
$ time juliaclient test.jl tsp_50.csv
3×2 DataFrames.DataFrame
│ Row │ x        │ y          │
│     │ Float64  │ Float64    │
├─────┼──────────┼────────────┤
│ 1   │ 0.420169 │ 0.628786   │
│ 2   │ 0.892219 │ 0.673288   │
│ 3   │ 0.530688 │ 0.00151249 │

real    0m18.596s
user    0m0.329s
sys 0m0.318s

But next time (and thereafter), it is a lot faster (I accept donations :-)):

$ time juliaclient test.jl tsp_50.csv
3×2 DataFrames.DataFrame
│ Row │ x        │ y          │
│     │ Float64  │ Float64    │
├─────┼──────────┼────────────┤
│ 1   │ 0.420169 │ 0.628786   │
│ 2   │ 0.892219 │ 0.673288   │
│ 3   │ 0.530688 │ 0.00151249 │

real    0m0.355s
user    0m0.336s
sys 0m0.317s

A reduction from 18s to 0.3s, the *next run only take a 2% of the original time.

Also, you can change the file and the performance is maintained:

test2.jl:

using CSV, DataFrames

fname = only(ARGS)
df = CSV.File(fname) |> DataFrame
println(last(df, 10))
$ time juliaclient test2.jl tsp_50.csv
10×2 DataFrames.DataFrame
│ Row │ x        │ y        │
│     │ Float64  │ Float64  │
├─────┼──────────┼──────────┤
│ 1   │ 0.25666  │ 0.405932 │
│ 2   │ 0.266308 │ 0.426364 │
│ 3   │ 0.865423 │ 0.232437 │
│ 4   │ 0.462485 │ 0.049489 │
│ 5   │ 0.994926 │ 0.887222 │
│ 6   │ 0.867568 │ 0.302558 │
│ 7   │ 0.475654 │ 0.607708 │
│ 8   │ 0.18198  │ 0.592476 │
│ 9   │ 0.327458 │ 0.354397 │
│ 10  │ 0.765927 │ 0.806685 │

real    0m0.372s
user    0m0.369s
sys 0m0.300s

Evaluate an expression on the server

Alternatively, a String can be passed to the server which is then parsed and evaluated in its global scope.

using DaemonMode

runexpr("using CSV, DataFrames")

fname = "tsp_50.csv";

runexpr("""begin
      df = CSV.File("$fname") |> DataFrame
      println(last(df, 3))
  end""")
3×2 DataFrames.DataFrame
│ Row │ x        │ y          │
│     │ Float64  │ Float64    │
├─────┼──────────┼────────────┤
│ 1   │ 0.420169 │ 0.628786   │
│ 2   │ 0.892219 │ 0.673288   │
│ 3   │ 0.530688 │ 0.00151249 │

Avoid conflict of names

The function names could conflict with the variable and function name of new files, because they are constants. In order to avoid any possible problem DaemonMode run all files in its own module to avoid any conflict of names.

Thus, if we have two files like:

# conflict1.jl
f(x) = x + 1
@show f(1)

and

# conflict2.jl
f = 1
@show f + 1

The DaemonMode client could run each one of them after the other one without any problem.

Running several clients at the same time

In previous versions, the server run one task for each client. However, since v0.1.5 DaemonMode is able to run each client in parallel. However, you can run the server function with the parameter async=false to have the previous behaviour.

$  julia -e 'using DaemonMode; serve(async=false)'

With the optional parameter async=true to server, the server run each client in a new task.

$  julia -e 'using DaemonMode; serve(async=true)'

That command will allow to run different clients parallel, but it will use only one CPU.

If you want to use several threads, you can do:

$  julia -t auto -e 'using DaemonMode; serve(async=true)'

Auto allows DaemonMode to use all processors of the computer, but you can put -t 1, -t 2, ...

The async mode have several advantages:

The main drawback is that the @show and logs in console can be send to the last task.

Automatically reload the modified packages

DaemonMode would execute the codes that are directly passed to the server, so each time the codes are updated, you would get the up-to-date results. However, sometimes you may also be developing some packages in the same time, and want they got reloaded when modified. You can use Revise together with DaemonMode for this purpose. You only need to add using Revise, before starting the DaemonMode server:

julia --startup-file=no -e 'using Revise; using DaemonMode; serve()'

Features

TODO