To focus on performance is a very nice idea. I’m happy to share my own findings
1. If you need to random access note/effect columns, do it via methods
Documentation clearly states that random access to the following objects is inefficient:
renoise.song().patterns[].tracks[].lines[]:note_columns[index]
renoise.song().patterns[].tracks[].lines[]:effect_columns[index]
renoise.song().instruments[].phrases[]:lines[index]
renoise.song().patterns[].tracks[]:lines[index]
Reason is, that the when asked for an item within an array, the API will first need construct the whole array and return it. After which, you then select single entry.
With such a simple case, it’s much better to access the object via it’s associated getter method:
renoise.song().patterns[].tracks[].lines[]:note_column(index)
renoise.song().patterns[].tracks[].lines[]:effect_column(index)
renoise.song().instruments[].phrases[]:line(index)
renoise.song().patterns[].tracks[]:line(index)
2. Schedule your updates
This also happens to be the golden rule of the Lua Gems book: Performance tip #1: “Don’t do it”, followed by #2: “Do it later”.
I do this throughout all my realtime-performing tools - it’s not hard, just involves a bit more code.
Basically, what you need to do is to centralize the updates that your tools needs to perform, and call this method through the idle notifier.
For instance, a simple tool might have a update method which would set user interface components to their most current state.
A more advanced tool could split the update method into several smaller bits, making each update a lighter task to perform.
The trick is that you raise a flag (a boolean variable) when the update needs to happen.
For example, the flag could be raised by specifying something like this:update_requested = true
Then, in a fraction of a second the idle notifier will pick this up and call the update method - and reset the flag
if (update_requested) then
-- do something cool, then reset flag
update_requested = false
end
The advantage is that you could ask for updates any number of times between idle notifiers, but the scheduled update will only happen once.
This approach can also be adopted to those situations where a notifier gets triggered very often. For example, the line notifier is famously flooding you with messages when you e.g. clear a pattern - one message for each line in each track. This can easily get into the hundreds - you would definitely want to schedule/delay the response in such a case.
3. Cache Renoise API methods and objects when possible
It’s a fact that the Lua language itself is extremely fast. Same is true for C++. The bottleneck is mostly_between_the two: everything has to be looked up, converted by luabind.
So, another way to improve performance of scripts would be to cache those objects that doesn’t change during the execution of a script.
For example, the call to renoise.song() is obviously a method call. And yes, this comes with a cost, albeit a very small one.
I’m pretty sure you wouldn’t see much of an impact with simple tools. But the larger a tool becomes, the more calls needs to be done, and things start to accumulate.
For this reason, it’s recommended that you avoid code like this:
for i = 1,100 do
if(renoise.song().patterns[i]) then
-- there was a pattern here
end
end
It can be very easily be changed into
local rns = renoise.song()
for i = 1,100 do
if(rns.patterns[i]) then
-- there was a pattern here
end
end
Some of my own tools have taken this to the extreme and contains just a single reference to the renoise song object used throughout the tool.
But this comes with a different kind of complexity as you’d then have to watch out that you don’t invoke any anonymous code that contain such references.
Why? Because the references will become invalid the moment that the song (or whatever you were referencing) is gone.
If you have worked with notifiers, you will know that referencing objects can be tricky - making sure that notifiers are always suppressed or removedwhen their associated objects have gone.
4. Profile your code
Urhg, this is the one aspect of programming that I find intensely boring. I actually prefer writing unit tests.
But yeah, it’s the scientific approach
–
- renoise.song().pattern_iterator:lines_in_song() is much “faster” than renoise.song().pattern_iterator:note_columns_in_song()
Well, they are different - the lines_in_song one doesn’t go as “deep” as the “note_columns_in_song”.
I would be surprised if the latter one performed worse than making the first one do what the second one does.
But you have a point with the “custom iterator”, if you know exactly what kind of task you set out to do you can optimize the hell out of it.
Reminds me a bit of ffx’ idea for a “jquery-alike” syntax… but I am sure he would find it too slow :lol:
Edit:
There are lot of examples where you see that the lua api in renoise is really slow
You’re right that in some cases, optimizing lua code is not the way forward. But when creating a gazillion menu entries, the API is not to blame - this is simply Renoise being slow in doing this particular type of task. I would view this as an opportunity to optimize Renoise - especially as this particular type of UI update can’t be scheduled (unlike others, such as updating the status bar)
Another way to thinking about improving API performance is to consider which things could be pulled into Lua, processed there, and then delivered back once done. Most obviously, this is true when processing samples - it’s done_frame by frame._If we could instead process chunks, we could avoid the overhead caused by the lua / C++ bindings.
Even with something like luajit (which otherwise promises a massive increase in speed), this overhead is causing that increase to be not so overwhelming - unless you are doing “pure” lua stuff.