TL;DR: Does multiple dispatch allow for a better alternative to plugins? If no, then a code generation gimmick is needed.
I am working on a framework and I would like to move most of the code into plugins. A plugin alters the behavior of the framework by implementing “hooks”. It has its own configuration and possibly state, and although it can work independently from other plugins, it may communicate with them. (E.g. several telemetry plugins create metrics and a monitoring plugin collects them).
I have a dynamic implementation that works, but it is not fast enough. There are a few hot points where the hooks need to be inlined. I did what my current understanding of Julia allows and come up with a possible solution, but I have two questions, a philosophical and a technical:
-
I have the gut feeling that the whole idea of a plugin is somehow not well aligned with multiple dispatch, and that a better solution may exist to the problem of building the framework out of composable chunks of code in a way that allows the user (application developer) to remove, replace or reconfigure any of the chunks easily. (Please check the code to see the functionality I feel essential)
-
If traditional plugins are the way to go, then: My solution is built on the idea of chained types (borrowed from layers in HTTP.jl) , but to allow plugin configuration and state, I instantiate the plugin types. So a plugin set looks like
PerfPlugin(LogPlugin(DefaultPlugin(), "Configured logger:")))
which has the typePerfPlugin{LogPlugin{DefaultPlugin}}
. The instances are also chained. This allows inlining, because for every hook the compiler knows which plugins in the chain implement it. For a few plugins this works great.
Unfortunately, to find the instance of the next plugin to call, intermediate ones must be skipped through, so the compiler generates amovq (%rax), %rax
for every skipped plugin (if the config/state of a later one is needed). This is a minor but not negligible issue, as having 20 plugins seems to be normal, and the hottest point I hope to make hookable is executed up to 4 million times per second.
How can I eliminate traversal of the chain?
You can try out the code at repl.it
# A framework with plugins:
abstract type Plugin end
struct DefaultPlugin <: Plugin end # Default behavior, implemented as a plugin
struct Framework{TPlugin}
firstplugin::TPlugin
end
next(plugin) = plugin.next
# A hookable operation
@inline step(framework::Framework) = step_hook(framework.firstplugin, framework)
@inline step_hook(plugin::Plugin, framework) = step_hook(next(plugin), framework)
@inline step_hook(::DefaultPlugin, framework) = sleep(rand() * 0.01)
# Plugin implementations:
using Dates
mutable struct PerfPlugin{Next} <: Plugin
next::Next
timesum::Millisecond
stepcount::UInt
PerfPlugin(next::Next) where Next = new{Next}(next, Millisecond(0.0), 0)
end
@inline step_hook(plugin::PerfPlugin{Next}, framework) where Next = begin
ts = Dates.now()
step_hook(next(plugin), framework)
timetaken = Dates.now() - ts
plugin.timesum += timetaken
plugin.stepcount += 1
println(timetaken)
end
# This can be called by plugins installed earlier in the chain
avgsteptime(plugin::Plugin) = avgsteptime(next(plugin))
avgsteptime(::DefaultPlugin) = NaN
avgsteptime(plugin::PerfPlugin) = plugin.timesum.value / plugin.stepcount
struct LogPlugin{Next} <: Plugin
next::Next
prefix::String
LogPlugin(next::Next, prefix="Default Logger:") where Next = new{Next}(next, prefix)
end
@inline step_hook(plugin::LogPlugin{Next}, framework) where Next = begin
println("$(plugin.prefix) Stepping...")
step_hook(next(plugin), framework)
end
# Does nothing, needs to be skipped
struct EmptyPlugin{Next} <: Plugin
next::Next
EmptyPlugin(next::Next) where Next = new{Next}(next)
end
# Application
println("-- a1: Perf plugin only:")
a1 = Framework(PerfPlugin(DefaultPlugin()))
step(a1)
println("\n-- a2: Log plugin(configured) only:")
a2 = Framework(LogPlugin(DefaultPlugin(), "Configured logger:"))
step(a2)
println("\n-- a3 Both plugins, two steps:")
a3 = Framework(LogPlugin(PerfPlugin(DefaultPlugin())))
step(a3)
step(a3)
println("Average step time: $(avgsteptime(a3.firstplugin)) milliseconds")
println("\n-- a4: three Empty plugins + 1 Log plugin")
a4 = Framework((EmptyPlugin(EmptyPlugin(EmptyPlugin(LogPlugin(DefaultPlugin()))))))
step(a4)
println("\n-- @code_native step(a4)")
using InteractiveUtils
@code_native step(a4)