Getting Started
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...
Installation
The package is registered, so simply: julia> using Pkg; Pkg.add("Plugins")
. (repo)
Your first and second plugins
The first one simply counts the calls to the tick()
hook.
using Plugins, Printf, Test
mutable struct CounterPlugin <: Plugin
count::UInt
CounterPlugin() = new(0)
end
Plugins.symbol(::CounterPlugin) = :counter # For access from the outside / from other plugins
Plugins.register(CounterPlugin)
function tick(me::CounterPlugin, app)
me.count += 1
end;
The second will measure the frequency of tick()
calls:
mutable struct PerfPlugin <: Plugin
last_call_ts::UInt64
avg_elapsed::Float32
PerfPlugin() = new(time_ns(), 0)
end
Plugins.symbol(::PerfPlugin) = :perf
Plugins.register(PerfPlugin)
tickfreq(me::PerfPlugin) = 1e9 / me.avg_elapsed;
When the tick()
hook is called, it calculates the time difference to the stored timestamp of the last call, and updates the exponential moving average:
const alpha = 1.0f-3
function tick(me::PerfPlugin, app)
ts = time_ns()
diff = ts - me.last_call_ts
me.last_call_ts = ts
me.avg_elapsed = alpha * Float32(diff) + (1.0f0 - alpha) * me.avg_elapsed
end;
The application
Now let's create the base system. Its state holds the plugins and a counter (just to cross-check the CounterPlugin
):
mutable struct App
plugins::PluginStack
tick_counter::UInt
App(plugins, hookfns) = new(PluginStack(plugins, hookfns), 0)
end
There is a single operation on the app, which increments the tick_counter
in a cycle and calls the tick()
hook:
function tickerop(app::App)
tickhook = hooks(app).tick
tickerop_kern(app, tickhook) # Using a function barrier to get ~5ns per hook activation
end
function tickerop_kern(app::App, tickhook)
for i = 1:1e6
app.tick_counter += 1
tickhook(app) # We can pass shared state to plugins on the hook. Here, for simplicity, the whole app.
end
end;
Running it
The last step is to initialize the app, call the operation, and read out the performance measurement from the plugins:
app = App([CounterPlugin, PerfPlugin], [tick])
tickerop(app)
@test app.plugins[:counter].count == app.tick_counter
println("Tick count: $(app.plugins[:counter].count)")
println("Average cycle time: $(@sprintf("%.2f", app.plugins[:perf].avg_elapsed)) nanoseconds, frequency: $(@sprintf("%.2f", tickfreq(app.plugins[:perf]) / 1e6)) MHz")
Tick count: 1000000
Average cycle time: 36.79 nanoseconds, frequency: 27.18 MHz
That was on the CI. On an i7 7700K I typically get around 19.95ns / 50.14 MHz. There is no overhead compared to a direct call of the manually merged tick()
methods.
You can find this example under docs/examples/gettingstarted.jl
if you check out the repo.
This page was generated using Literate.jl.