call MIDI input script functions with appropriate "this" object#919
call MIDI input script functions with appropriate "this" object#919rryan merged 7 commits intomixxxdj:masterfrom
Conversation
|
Won't this cause performance problems if a script makes liberal use of the "this" keyword? Or is resolveThisObject() intended to be called just during initialization? |
|
I modified the commit since opening the PR. I realized there wasn't a need for a new function at all. resolveFunction's caching works this way and the function can still write properties to the "this" object. I don't know why the caching I tried when I had it in a separate resolveThisObject function didn't work. |
|
Do you think it would make sense to rename resolveFunction() to resolveObject()? |
|
Thank you for working on this. I think we need to to some more refactoring to not introduce a performance regression. It looks like for your approach ControllerEngine::resolveFunction is broken. It looks like resolveFunction should return m_pEngine->globalObject() , if there is no obj found in the "obj.func" schema. |
|
What is the performance concern with the way it is? Making two function calls rather than one? How would you recommend returning two values, with a std::pair? tuple? QHash? |
The function body (script usage of this) doesn't matter since this is already resolved at that point -- the only performance concern is that it adds an additional environment inspection per incoming MIDI message. |
|
resolveFunction() might be longer than the final script call by dimensions. |
|
Don't use a std::pair, tuple or similar anonymous structs. These are only useful for obfuscation. In this case you may consider to call the execute from inside the resolveFunction or use output parameters. |
|
Thanks for working on this! I've also been bothered by the right way to get 'this' pointing to something that makes sense in script handlers. I think eventually we want to get away from resolveFunction and instead eval whatever expression the XML gave us. If the result of that eval is a function then we call/cache it. Doing dot-based environment traversal is kind of doing the work of eval but probably inaccurately. The "function prefixes" we use for init/shutdown are particularly hacky too and I'd like to see them go. I think we might also encourage script writers to properly bind their event handlers to the object the handler lives in using Function.prototype.bind. We could even provide a helper method to bind handlers in our common Controller class we provide. Then it wouldn't matter what context we execute the method with. Anyhow.. what we do is roughly the equivalent of: function Controller() {
this.name = "FancyController";
}
var MyController = new Controller();
MyController.handler = function() {
console.log(this.name);
}Mixxx then does the rough equivalent of: var theHandler = eval("MyController.handler");
theHandler();And you get: OTOH if you do: eval("MyController.handler()");You get" If the function were bound: MyController.handler = function() {
console.log(this.name);
}
MyController.handler = MyController.handler.bind(MyController);
var theHandler = eval("MyController.handler");
theHandler();Then it would work as expected: |
How about storing the QScriptValue for the "this" object for each function name in a cache along with the QScriptValue for the function? |
I'd rather that Mixxx found the appropriate |
I agree. I think this could also resolve https://bugs.launchpad.net/mixxx/+bug/1565377 . It could also allow snippets of JS to be entered in the mapping GUI. |
|
Yes, we should do this. We have actually real performance issues with High resolution jog wheels here: |
Yes, I am aware. I do not have any SCS controllers or controllers with jog wheels though, so others would have to test if changes impact real usability in such demanding contexts. |
|
Hm, I realized that it's kinda silly to handle this upon receipt of every MIDI message. It would make more sense to store the QScriptValues in a cache upon loading the mapping, then refer to that when MIDI messages are received. |
|
We do not need a SCS to check the performance changes. It is obvious that every "." in the sting is an extra cache lookup. |
|
Here's how the standard says this should work: So this describes why foo.bar(1, 2, 3)calls bar with this set to foo but var f = foo.bar;
f(1, 2, 3);calls bar with this set to the global object. The grammar production rule for function calls distinguishes between an object reference and a free standing value. When foo.bar is part of the expression bar is an object reference but when it's a standalone value it's not. Our traversal of the environment and call of bar is equivalent to the latter example. To get normal javascript behavior, we probably want the former. Using the prior value in the dot traversal would mostly approximate the GetBase function defined by the standard for object references so this is a pretty reasonable fix! However it wouldn't work for foo.bar[foo.currentDeck].handlerFor that we probably need to build a Javascript expression parser to do it right (notice how splitting on dots no longer works for that case). |
|
Also note that if we want to support: foo.bar[foo.currentDeck].handlerthis means caching is no longer an option since we need to re-evaluate foo.currentDeck each time to be correct |
|
|
||
| QScriptValue function = pEngine->resolveFunction(mapping.control.item); | ||
| if (!pEngine->execute(function, channel, control, value, status, | ||
| QScriptValue thisObject = pEngine->resolveFunction(mapping.control.item.section(".", 0, -2)); |
There was a problem hiding this comment.
please also update sysex handling (line 551) for consistency
|
So I would be in favor of merging this as a short term fix. To minimize perf impact, resolveFunction and the script value cache should probably be updated to return a std::pair of (context, value) where context is the last value in the dot chain before value -- that way we'd be doing essentially no more work than we already are per MIDI message. |
|
Okay, I'll work on that as a short-term fix. I think trying to implement fully correct JS behavior by parsing strings in C++ would likely end in other hacks that don't really work with the full flexibility of JS. I think to really take advantage of all JS has to offer, the input handler callbacks need to be registered by scripts (generally in their init functions). Then getting the correct |
|
Ok, I understand we cannot cache the object because it may vary. But I think there are some possible ways to solve it:
Pseudo code: If we found out the compiler is smart enough to ditch the extra call, we can always use the evaluate.
never use std::pair is a source of later confusion. These are good alternatives
|
Hm, I disagree with "never use std::pair". It's not too hard to read the function documentation to learn what it returns -- it's the same as reading the function prototype to learn what the output parameters are or reading the struct definition to learn what members it has. I agree that a pair isn't good to use as a data model that is prevalent across the codebase but for an internal implementation detail it's fine. |
Unless we have data showing an extra pre-evaluated-by-the-interpreter function call is worse than what we have now with resolveFunction poking at the environment repeatedly from the QtScript API then I wouldn't worry about it. Javascript interpreters are fast from years of optimizations trying to get fancy webapps to work faster and I'm not sure our QtScript API and string manipulation would win against a JS interpreter's evaluation of the user provided expression -- especially if the code is already parsed (which it would be after our one-time evaluate) and better yet it would be a correct implementation of the JS spec! On the whole wrapping every script handler in an anonymous function is a pretty nice solution and only requires the string formatting once up front. Pro:
Con:
|
|
There are situations where pair helps, but they are very rare. You can use std::tie to make it less scary. But even that involves some anonymous boiler plate which should be avoided. In this case this reads well: If the pointers are to scary for you we may use This is far more self explaining than |
|
So here's my proposal based on @daschuer's -- at mapping initialization time turn every handler into an anonymous function and evaluate it once then and not when MIDI messages come in: QString wrapper = "function (channel, control, value, status, group) { (" +
mapping.control.item + ")(channel, control,value, status, group); }";
m_pEngine->evaluate(wrapper); |
I agree that if the type will spread beyond the file it lives in then it's best to define a type for it. However I think we're going to have to agree to disagree on whether it's ok to use std::pair in local contexts. |
Could you type |
|
Full backtrace attached |
|
I'm using qt-4.8.7-12.fc23.x86_64 |
Also, print error messages through JS functions
|
Okay, wrapFunctionCode() always caches the QScriptValue function now. |
| } | ||
| function.append(".incomingData"); | ||
| QScriptValue incomingData = m_pEngine->resolveFunction(function); | ||
| QScriptValue incomingData = m_pEngine->wrapFunctionCode(function, 2); |
There was a problem hiding this comment.
This loop should be moved to:
ControllerEngine::initializeScripts and fill a simple QList m_incommongDataFunctions
which is only called once.
Here we can use than a simple .. without any string arithmetic:
for (const auto& incomingData: m_pEngine->m_incommongDataFunctions()) {
if (!m_pEngine->execute(incomingData, data, timestamp)) {
qWarning() << "Controller: Invalid script function" << function;
}
}
…Code() instead of returning a JS function that prints an error. This avoids having to deal with escaping JS within JS within C++.
|
Okay, I found a way around having to deal with escaping JS within JS within C++. This still gives meaningful error messages on every button press in the log, but only pops up an attention-grabbing dialog the first time the button is pressed. Note that this requires doing the m_pEngine->evaluate() before checking the syntax because m_pEngine->checkSyntax() does not generate an exception. |
|
|
||
| if (functionObject.isError()) { | ||
| qDebug() << "ControllerEngine::internalExecute:" | ||
| << functionObject.toString(); |
There was a problem hiding this comment.
It would be easy to call scriptErrorDialog(functionObject.toString()) here, but IMO popping up a dialog repeatedly is annoying. When debugging a mapping, I'd rather look through the log than have to click to close a bunch of dialogs.
Thanks -- that's an odd one: I think this is probably related to google-test parsing of commandline flags (it mutates argc) and QApplication's use of command line flags. Can you try messing with this line: For example:
Not really sure what's going on. Maybe google test is dropping argc to 0. QApplication says argc needs to be at least 1. I tried on Mac and Linux and argc is always 1 when I invoke mixxx-test as "./mixxx-test". |
|
Actually, I don't think wrapFunctionCode() needs to check syntax. The "Parse error" message is generic, but MidiController::processInputMapping prints the offending code snippet in the log with each button press. Passing the QScriptValue Error to execute() then internalExecute() prints "Parse error" along with each button press as well. That may be difficult for many users to understand, but I think mapping developers would know to look at the log and then they'd see quite clearly what happened. |
|
The exception error messages from evaluating the wrapper look pretty much the same as those for just executing codeSnippet, so yes, it is fine to just evaluate the wrapper. |
Cool, ok -- this LGTM. Thanks for the changes! |
|
Great! I'll add a couple more things to the P32 mapping not depending on this that will be compatible with 2.0, then I'd like to branch off from that and take advantage of |
Potentially fixes mysterious mixxx-test segfaults that look like this: ... and #919 (comment) I think this is the cause. QApplication::QApplication takes argc as a reference and we're passing it an integer that lives on the stack.
I think I fixed the problem. Uggggggh. 8e905c7 |
Fixing bug https://bugs.launchpad.net/mixxx/+bug/1567203