I am thrilled to announce the result of my first contact with staged programming: Plugins.jl
Let’s say you are writing a lib which performs some work in a loop quickly (up to several million times per second). You need to allow your users to monitor this loop, but the requirements vary: One user just wants to count the total number of cycles, another wants to measure the performance and write some metrics to a file regularly, etc. The only requirement that everybody has is maximum performance.
Maybe you also have an item on your wishlist: To allow your less techie users to configure the monitoring without coding. Sometimes you even dream of reconfiguring the monitoring while the app is running…
Plugins can be found everywhere, including much-used Julia packages like Documenter.jl, PkgTemplates.jl, Genie.jl and HTTP.jl (called Layers).
Plugins can help to build flexible and extensible architectures, but their dynamic nature comes with a performance burden that prevents their use in really hot code.
This is the problem that Plugins.jl solves.
There is no standard definition of the word “plugin”, here is the one used by Plugins.jl:
A plugin (aka extension) is a chunk of code that extends the functionality of a system. The plugin has its own lifecycle and it implements so-called hooks to react to events generated by the system. If multiple plugins implement the same hook, they will be called in their order, with any plugin able to halt the processing.
A very simple example with a plugin that counts the calls to the
mutable struct CounterPlugin <: Plugin count::UInt CounterPlugin() = new(0) end Plugins.symbol(::CounterPlugin) = :counter function tick(me::CounterPlugin, app) me.count += 1 return false # return true to interrupt hook processing end
And an “application” that calls the
tick() hook in a tight loop:
mutable struct App plugins::PluginStack App(plugins, hookfns) = new(PluginStack(plugins, hookfns)) end function tickerop(app::App) tickhook = hooks(app).tick tickerop_kern(app, tickhook) # Using a function barrier to get ~7ns per hook activation end function tickerop_kern(app::App, tickhook) for i = 1:1e6 tickhook() end end const app = App([CounterPlugin()], [tick]) @btime tickerop(app) println("Total Tick Count: $(app.plugins[:counter].count)")
221.766 μs (0 allocations: 0 bytes) Total Tick Count: 27580000000
1 million hook calls in 0.222 ms (That is ~4.5 GHz, the clock speed of my proc).
A little more lifelike example with two plugins on the same hook can be found in the getting started guide.
- Merging hook implementations into a single function body
- Skipping non-implementing plugins with zero cost
- Hook implementations can prevent later ones from running by returning true (like DOM event handlers)
- Access of the loaded plugins by their registered symbol (from other plugins or from the outside)
- Passing a shared state and arbitrary number of arguments to hook implementations
- Adding or removing plugins dynamically
- A fast cache of merged hooks that gets rebuilt when the plugin stack changes
eval() or any compiler magic was needed to make this, only a chain of parametric types. The main idea was borrowed from HTTP.jl’s layers.
I hope that this package will be useful to the community!