Hi,
I am thrilled to announce the result of my first contact with staged programming: Plugins.jl
Motivation
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.
Example
A very simple example with a plugin that counts the calls to the tick()
hook:
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.
Features
- 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
Interestingly, neither @generated
nor 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!