Poor performance of LiveAPI in JS object
I'm working on a Max for Live device for setting custom colours on my Launchpad.
I'm using JavaScript, there's basically one JS object in my device that does everything.
In this object, I have lots of observers. I'm basically observing every slot in session mode to detect if it has a clip or not, and if that status changes, and I'm observing every clip for changes of its colour index.
It works, but it is quite slow. When the device is loaded, I can watch the Launchpad's pads changing their colour one after the other.
Is there something I'm missing? Are there best practices to speed up setting up LiveAPI observers?
If you're curious, I've put my JS code on GitHub, you can take a look here.
Thanks in advance for your help!
Hi Patrick,
I like the idea of using TypeScript for JS, especially in combination with the Live API - this reminds me on some ideas I had in the past, to use my Push-2 in more creative ways.
I can't give you an advice regarding the Live API in JS, but maybe my questions help you to find the root cause of your issue.
1) As you are using TypeScript, are your sure that the transpiled JavaScript does not contain any "humbled code" ?
It's some time ago I researched about which TypeScript compiler settings should be used to generate JavaScript that can be understood by the JS object - what's the source of your settings for tsconfig.json to spill out the right JavaScript?
2) What about about a mockup of the Live Observer API?
As you are implementing in TypeScript it should not be that hard. Replacing (simulating) it with something that you can control from an UI inside you M4L device, like e.g. a matrixctrl, sending the on -> off transitions into your JavaScript mockup that in turn triggers Live Observer alike data.
The question is : where do this "callback alike creations" exist, that a Live Observer API creates when setting it up in JS? Is there a difference to their setup via Max objects (inside a patcher) or not?
I guess (but I don't really know) there is no difference, as the JS object most probably will call a native M4L API - or in other words, the Live API is not replicated or re-implemented in JS. The API is just a bridge.
So chances are, that the cause of the latency is something different, as observer events are just single events - they have no aspects of real time processing lots of data, so crossing the thread boundaries as mentioned in this post should not be that noticeable or the problem.
3) maybe its your Launchpad or the API used for accessing it?
What about a mockup of the target - using a matrixctrl (or grid of live.text buttons) as a Launchpad simulation to see, if the response will be faster?
Hi Patrick,
I just rebuild your Yarn based setup with Npm and implemented a simple Hello world alike JS object and it works just fine.
Thumbs up for your setting this up and thanks sharing this with us - this TypeScript pipeline is very valuable and makes using the js object way more comfortable ... awesome work!
I will implement some Push-2 related code, observing all the note pads and giving it a light show as well, and I will report back how the push-2 behaves and if I experience any latency as you do.
In the meanwhile I am curious about the lots of webpack code stuff in the transpiled JavaScript. Is this bunch of code really needed and when it is need, why or for what?
TypeScript Source :
export function bang() {
post('Hi from TypeScript');
}
transpiled JavaScript :
var maxJsObject = this;
var observeSelectedMidiClip;
/******/ (function() { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 390:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
var createBinding = (this && this.createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var exportStar = (this && this.exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exportStar(webpack_require__(404), exports);
/***/ }),
/***/ 404:
/***/ (function(__unused_webpack_module, exports) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
maxJsObject.bang = bang;
function bang() {
post('Hi from TypeScript');
}
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module is referenced by other modules so it can't be inlined
/******/ var __webpack_exports__ = __webpack_require__(390);
/******/ observeSelectedMidiClip = __webpack_exports__;
/******/
/******/ })()
;
Hi Patrick,
well, its more complicated than I thought - the webpack code seems to be crucial and also how to build the code or which files to import.
Trying to use a simple TypeScript class (Foo) that holds nothing more than a number and calling its constructor fails - sending "setup" to js objects result in an error : ... Javascript TypeError: foo_1.Foo is not a constructor, line 47. Which is : var foo = new foo_1.Foo(42);
Pasting all the code makes no sense, this would be too much, so just the basics down below. Does this error make any sense to you - I am missing a fundamental setup / import in the code (including "core-js" or not, does not make a difference)?
I am using a stripped down version of your Github Repo and have only replaced Yarn with Npm and I get to warnings from the webpack build.
And by the way - this is was I meant with "humbled code" in my first reply ... these transpilation from TypeScript to JavaScript are not seldom a source of surprise and reading that transpiled code is all, but no fun ;-).
The TypeScript Code :
//import "core-js";
import { Foo } from "../../utilities/foo";
autowatch = 1;
outlets = 2;
const controller = 41;
export function bang() {
post('Hi from TypeScript');
}
export function setup() {
let foo = new Foo(42);
post(`setup called - controller is = ${controller} and foo has value of ${foo.value}\n`);
}
I think the problem here is that observers are computationally expensive, you have many of them, and they run in the low priority thread so they will get spread out over time to prioritize other work.
I don't think TypeScript is going to change that at all. Personally, and I get many people will disagree, I don't see any advantage to TypeScript in Max and Live - to me the advantage of typed languages come to the fore for larger teams working on large code bases. In my experience, dynamic typing is much more helpful when doing solo, small projects. But YMMV on that!
I can definitely say it won't help the issue you are describing though, and I would be heavily that implementing it in Max will give you the same issues. Observers are just heavy.
Hi everybody,
thanks for your kind feedback on my project and for taking the time to write responses to my questions!
Iain, you wrote:
I don't think TypeScript is going to change that at all
I didn't think it would. In the end, webpack is producing JavaScript code, so it would be illogical to hope that this would be more performant. I'm just using TS as a way to write code that's easier to maintain in the long run.
Regarding the computationally expensive observers, yes, that's what I feared. Is there a way to make the JavaScript run in a higher priority thread?
Soundyi, regarding your comment:
In the meanwhile I am curious about the lots of webpack code stuff in the transpiled JavaScript. Is this bunch of code really needed and when it is need, why or for what?
Webpack bundles up all the modules into a single JS file that can be used as a JS object in Max. This is how I'm refactor parts of the code into separate TS modules in the utils package, to provide shared functionality for all the objects.
what's the source of your settings for tsconfig.json to spill out the right JavaScript?
The "magic" is not in the TS config :-)
I wrote a little webpack plugin to make it produce JS bundles that can be used in Max: https://github.com/zapperment/max-for-live-lib/blob/7905b002a9ca0aaac2806693c06d5b68e8430231/src/webpack/MaxForLivePlugin.js
This makes it so that all the exported functions from the objects, e.g. bang in the betterLaunchpadColours object, are exposed as global variables in the JS bundle, so that Max can reference it to map incoming messages.
Webpack does create a lot of boilerplate code, I don't think that can be helped. Perhaps other bundlers like Vite or Rollup would produce better results. But honestly, I wasn't ever planning on looking at the code that webpack produces, I only work with the TypeScript modules. Max doesn't seem to mind the blown up JS code.
Trying to use a simple TypeScript class (Foo) that holds nothing more than a number and calling its constructor fails - sending "setup" to js objects result in an error : ... Javascript TypeError: foo_1.Foo is not a constructor, line 47. Which is : var foo = new foo_1.Foo(42);
How are you exporting the Foo class? The way you are importing it, it should be with export class Foo
(not export default Foo
)
Besides that, there's nothing extra to it. I'm also using a class here, no problem importing and instantiating it: https://github.com/zapperment/max-for-live-lib/blob/better-launchpad-colours/src/util/ApiManager.ts
including "core-js" or not, does not make a difference
core-js is a library that polyfills some of the newer features of JavaScript that are not (yet?) supported by Max's JS engine. I use it for array methods like "fill" and "find".
I included my own polyfills for these at first (https://github.com/zapperment/max-for-live-lib/tree/better-launchpad-colours/src/polyfills), but then I thought, "hey, why reinvent the wheel?"
Still not 100% sure if including core-js in its entirety is a good idea, it increases the bundle size of the JS object a lot.
are your sure that the transpiled JavaScript does not contain any "humbled code" ?
this is the first time I'm hearing this term "humbled code", can you point me to some resource that explains it?
2) What about about a mockup of the Live Observer API?
Ooooh, that sounds interesting, I'll look into it.
My Max game is still weak, that's why I try to do everything with TypeScript, which I feel more at home with. But Max is a fascinating language.
Meanwhile, I've actually given up trying to fix the performance issue and control my Launchpad's colours with M4L.
Instead, I'm trying to hack the MIDI remote surface script that comes with Live with a hex editor, to fix the source of the problem rather than using M4L as a workaround. But that's a whole different kind of rabbit hole…
3) maybe its your Launchpad or the API used for accessing it?
Hm, I don't think so, the launchpad colours are controlled by simple MIDI control change messages.
Just so you're aware, there are disassembled control surface scripts in Python available around online (not sure which ones mind you). Years ago I made some for my launchpads that way.
Iain, this surprises me (much more than the statements which followed it):
observers are computationally expensive,
Is this something you measured independently of (potentially expensive) callbacks to the observers? In particular having lots of observers on properties which only rarely change doesn't strike me as something that should have a significant cpu hit. They respond to notifications rather than poll for changes, no?
(I also long ago made a JS-based push controller (without using any live observers) and gave up on that approach because, as you point out, the low-priorityness of its processing quickly becomes untenable when the cpu gets busy.)
@Iain
Just so you're aware, there are disassembled control surface scripts in Python available around online (not sure which ones mind you). Years ago I made some for my launchpads that way.
Yes, thanks, I'm already aware 😀 However, there aren't any for the latest control surface scripts from Novation.
I think this is because these are compiled for Python version 3.11, and the most popular decompilers decompile3 and uncompile6 can only decompile Python versions up to 3.8.
I managed to at least partially decompile the files with pycdc and disassembled them to bytecode disassembly with pycdas.
From the disassembly and partial source, I was able to reconstruct the full source code of the module that defines the colour mapping from Live clips to Launchpad Pads.
That's how I managed to modify the colours on the Launchpad to my liking, which I was originally trying to achive with Max for Live 🥳
Not sure if these are useful, but I recently decompiled the Live 12 python midi scripts: https://github.com/shakfu/live-midi-scripts
@Shakeeb
Not sure if these are useful, but I recently decompiled the Live 12 python midi scripts: https://github.com/shakfu/live-midi-scripts
Wow, this may be super useful for other projects! I wish I had found this sooner.
How did you manage to decompile? I see you've used decompyle3, I couldn't get that to work.
Responding to Tyler: no I did not do enough tests to figure out if they are expensive if the thing being observed is not changing frequently, so thats a good point. My initial experiments with them didn't work out so well, so I switched to a different architecture, listening to plugsync~ and maintaining state in my scheme scripts.
@Patrick Hund
Great, glad you find the scripts useful!
With respect to using decompyle3, I'm not sure I remember having any major problems using it at the time.. I was a bit surprised about how well it worked..
S
Hi Patrick,
just a quick response and to clarify my strange "bug report" regarding the creation of objects (calling the Foo constructor) : I guess its a problem with the JS cache of the Max Editor.
It took me quite a while to figure this out and I got as I changed gears and used the NPM package TypeScript for Max : https://github.com/ErnstHot/TypeScript-for-Max.
After that things (the transpiled JavaScript code) got more transparent and I noticed that even sending "compile" to the js object did not solve error message I experienced from time to time, but after I close the Max editor and restarted Live everything was fine.
So it seems "autowatch = 1" does not work in all circumstance and I haven figured out, which constellations make problems - I can imagine that importing script makes trouble, as the autowatch only looks a the primary .js file.
But anyways - the solution is : if an error message pops, that should not happen, save & restart Live.
Hm, I haven’t encountered any issues with autowatch (yet?)
I can imagine that importing script makes trouble, as the autowatch only looks a the primary .js file.
The bundle that is produced by web pack does not import anything, it’s a single JS file that contains all the code that in typescript is in separate modules. That’s the whole point of using webpack 😀
Thanks for the tip with the Typescript-for-Max repo!
Back when I originally set up my project, there weren’t any types for max available. I’ve updated it now to use the types from the types/maxmsp package.
I’m not using the original package but copied it over and made some adjustments — it seems the types from the package are outdated or even incorrect in some places. For example, it says that LiveAPI.call doesn’t return any value, but it clearly does, for example for ControlSurface.get_control_names
The bundle that is produced by web pack does not import anything, it’s a single JS file that contains all the code that in typescript is in separate modules. That’s the whole point of using webpack 😀
Sure 🙃 - what I was talking about were not problems with your library, but with the setup of the TypeScript for Max approach and its build pipeline.
As I got similar "named" issues as the one I posted regarding your library, I thought I post about it, but I forgot to make it 100% clear that this was related to using the TypeScript for Max approach.
And this is still an issue for me from time to time (using the TypeScript for Max approach) and its serious, because its happens out of nowhere and shows strange results, like empty outputs in the Max console - so it does not always end up in undefined methods or properties exceptions.
So your approach using a bundler makes sense, and as I use something similar for my node.script development with esbuild, I will incorporate this at some point in the future too, but first I will try to narrow this down when & why this happens, as I hope that this will give be a better understanding about how the Max Editor handles JavaScript stuff.
But I think that's something for another and separate post / forum thread.
+++
And regarding performance of using the Live API in JavaScript, I experience no issues so far, using the TypeScript for Max approach.
So far I am observing all 64 Note Pads of the Push-2 and some other objects of the LOM, all from JavaScript and it works just fine.
I also change the colors of the note pads, when I activate my custom push layout and its quite fast. As I do this in a for loop, it looks like a fast lowering of a jalousie, so there is some latency, in the call chain from JS -> Max 4 Live -> (Python) remote Script -> USB -> Push 2, but my feeling is, its fine.
And most importantly : I experience no latency the other way round. So pushing a note pad on push is immediately there - as far as I can hear it, as I trigger sounds (and see, as I change the color of the pads). For sure there will be some ms latency as this has to go through USB -> (Python) remote Script -> Max for Live -> JS, but for me, it seems accurate so far.
But I have not done intensive tests, as I am still in the development of the Push Layout / M4L device.
+++
What also came to my mind regarding performance : several instances observing the same LOM object might be an issue.
E.g. I deactivate my custom Push Layout, when I change the selected track in Live, so that I can play with the factory layouts on the other tracks, and when I come back to the track in Live, that hosts my M4L device, this custom Push Layout is activated again.
So regarding my code & workflow, there happens a clean up of object references that implement this LOM object observations, and it seems to work - setting this object references to null seems to trigger a memory clean up and hence release of the Live API resources in the background (Max for Live layer).
@Patrick Hund
I have to correct myself 🙃 - it's not sufficient to set a JavaScript object, that host a Live API object inside, to null to free all resources.
Currently I experiment with another way of using the Live API in JavaScript and stumbled over this behavior.
If you observe a value via an instance of the LiveAPI e.g. instantiated with a path of "live_set view", setting liveApi.property = 'detail_clip' to observe the selected clip (or as docs say : The clip currently displayed in the Live application's Detail View.) and use some class that encapsulates the LiveAPI object (I have not tested, if its also the case without the wrapper class), it's NOT enough to set the instance of the wrapper class to null to free all Live API resources.
You have to reset the property "property" of the Live API object to an empty string first (liveApi.property = ''), in order to stop the observation - otherwise you will receive further updates although the instance is already out of scope (or without a variable reference).
I guess this can lead to performance issues and a kind of memory leak, if this code is called several times, like e.g. each time on selecting the track the M4L device sits on - believing the instances and all event listeners / Live API resources are released, by resetting the instance variables to null, when deselecting the track.
This make me wonder if there even more pitfalls using the Live API inside JavaScript, because of the way it is implemented (invisible for us) in Max for Live. I am not aware of how the ancient JavaScript Engine that Max uses up to version 8, handles heap memory and if Cycling 74 has implemented some interceptors to clean up their resources.