Difference between revisions of "Debug OXP"

From Elite Wiki
(Added "Profiling" section)
(The console script: Updated the example and added a section about using variables)
Line 210: Line 210:
 
Logs the hierarchy of calls made, which can help track down where an error is occurring.
 
Logs the hierarchy of calls made, which can help track down where an error is occurring.
 
-->
 
-->
 +
 +
=== Variables in the console ===
 +
You may want to save the result of a command so that you can use it in one of the next commands. There are several ways to do this, as well as several pitfalls.
 +
 +
First of all, as var, let and const limit the scope of the variable to the enclosing function, they do it as well for command scope, so do not use them if you want to reuse the result. In most cases, instead of this just assign the result to the variable without declaration - it will be stored in the <code>[[Oolite JavaScript Reference: Global | global]]</code> object:
 +
> a // Check to ensure that the variable is not defined
 +
Exception: ReferenceError: a is not defined
 +
> a = 123
 +
123
 +
> global.a
 +
123
 +
> a
 +
123
 +
> global.a = 456
 +
456
 +
> a
 +
456
 +
 +
It's the simplest way, and it has one minor pitfall - the variable name must not overlap with existing global properties from the Oolite or other OXPs.
 +
 +
You can also use the console script's <code>this</code>. After defining the variable as a property of <code>this</code> it can be used without explicit <code>this<u>.</u></code>. Property of <code>this</code> hides property of <code>global</code>. See the example (continues the previous):
 +
> this.a = 789
 +
789
 +
> a
 +
789
 +
> global.a // still has the old value
 +
456
 +
 +
Note that <code>this</code> is [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this not a variable but keyword], and has somewhat non-obvious behaviour when used inside of functions (the example continues the previous):
 +
> f = function() { return a } // There is no local a, so it uses one from the command's this, if it wasn't there, it will it would use one from the global scope.
 +
> f()
 +
789
 +
> f = function() { return this.a }
 +
> f() // function called not as a method, so uses "global" as "this" (only in nonstrict code)
 +
456
 +
 +
 +
==== Predefined and special variables ====
 +
The console also offers a few short aliases by default:
 +
P = player;
 +
PS = player.ship;
 +
S = system;
 +
M = missionVariables;
 +
They added in the <code>this</code> way and you can add your own - check the next section for it.
 +
 +
 +
The special variable <code>$</code> stores the last non-<code>undefined</code> result:
 +
> system.addShipsToRoute("trader", 1, 1)
 +
<nowiki>[[Ship "Boa" position: (-105926, 56511.4, 896027) scanClass: CLASS_NEUTRAL status: STATUS_IN_FLIGHT]]</nowiki>
 +
> $[0]
 +
<nowiki>[Ship "Boa" position: (-105926, 56511.4, 896027) scanClass: CLASS_NEUTRAL status: STATUS_IN_FLIGHT]</nowiki>
 +
> $.dataKey
 +
griff_boa_alt-NPC
 +
> $
 +
griff_boa_alt-NPC
 +
  
 
=== The console script ===
 
=== The console script ===
Line 223: Line 279:
 
  {
 
  {
 
     // Ensure debug console is installed
 
     // Ensure debug console is installed
     if (!debugConsole)  return
+
     if (!console)  return
 
      
 
      
     // Save original implementation of consolePerformJSCommand
+
     // Save the original implementation of consolePerformJSCommand
 
     // Note the prefix, used to ensure uniqueness. If several scripts patch
 
     // Note the prefix, used to ensure uniqueness. If several scripts patch
 
     // the same method, it's important that they use different names.
 
     // the same method, it's important that they use different names.
     debugConsole.mySuperConsoleMacros_original_consolePerformJSCommand = debugConsole.consolePerformJSCommand
+
     console.script.mySuperConsoleMacros_original_consolePerformJSCommand = console.script.consolePerformJSCommand
 
      
 
      
     // Replace debugConsole.consolePerformJSCommand with custom function
+
     // Replace console.script.consolePerformJSCommand with custom function
     debugConsole.consolePerformJSCommand = function(command)
+
     console.script.consolePerformJSCommand = function(command)
 
     {
 
     {
         // Note that in this function "this" will refer to debugconsole, not
+
         // Note that in this function "this" will refer to console.script, "oolite-debug-console", not
         // my-super-console-macros, since the function will be called as a
+
         // "my-super-console-macros", since the function will be called as a
         // method of debugConsole.
+
         // method of the console.
 
          
 
          
         // Strip leading spaces, same code as original function.
+
         // Strip leading spaces, same code as the original function.
 
         while (command.charAt(0) == " ")
 
         while (command.charAt(0) == " ")
 
         {
 
         {
Line 250: Line 306:
 
         else
 
         else
 
         {
 
         {
             // Otherwise, call through to original method.
+
             // Otherwise, call through to the original method.
             debugConsole.mySuperConsoleMacros_original_consolePerformJSCommand(command)
+
             console.script.mySuperConsoleMacros_original_consolePerformJSCommand(command)
 
         }
 
         }
 
     }
 
     }

Revision as of 21:01, 26 May 2023

If you are developing OXPs, the Debug Console is a tool that you should keep handy at all times, because it enables you to test things that normally are either impossible to test without it, or require substantial effort to generate the right conditions for testing. Debug.oxp enables JavaScript console support in Oolite (1.70 and later). It also adds a menu with various debug facilities under Mac OS X.

To run this OXP you need the OXP Developer release version of Oolite. There are subtle differences between the versions for each of the three platforms (AppleMac, Windows & Linux).

Downloads

Basic-Debug OXZ for Oolite v1.83/84

External JavaScript console support

On all platforms, installing Debug.oxp enables support for external console applications using the Oolite debug console TCP protocol. By default, it will attempt to connect to a console running on the same computer, but this can be changed by specifying a different console-host (and, optionally, console-port) in debugConfig.plist. For external console support on the same computer under Mac OS X, console-host must be explicitly set to “127.0.0.1”; if this is not done, the integrated JavaScript console will be used instead. For information on using a console (integrated or external), see Using the JavaScript console below.

Note: A cross-platform, python based, external console application is available in the Oolite GitHub project area.

It contains a precompiled console for windows based computers, and the original python based console for Linux and Posix compliant computers.

On Debian or Ubuntu Linux you will need python 2.6.x installed and (at least) the packages blt python-tk tk8.5 (or the equivalents in other distros.)

Mac OS X-specific features

Under Mac OS X, the Debug OXP provides a menu with various debugging options and an integrated JavaScript console, as well as enabling external console support as on all platforms. This was originally written by Jens Ayton/Ahruman for version 1.69 (TR) back in 2007. To use this you need the Debug OXP in your AddOns folder and the Test Release version of Oolite.

Debug menu (Mac OS X only)

Oolite-debug-menu.png

The Debug menu provides options to

  • Show the Oolite run log in the standard Console application.
  • Enable or disable various classes of log message at runtime (like editing logcontrol.plist, without the need to restart Oolite).
  • Enable various testing features, such as drawing of bounding boxes.
  • Show the JavaScript console, and specify when it should be shown automatically. (This may not have any effect with external consoles, depending on the external console implementation.)
  • Create a ship of any role near the main station (equivalent to the :spawn macro).

Note: some of these features are implemented by sending JavaScript messages to the console script. If the script has been modified, some features may not work properly.

The Log Message Classes submenu can be modified by editing debugLogMessageClassesMenu.plist, found inside the OXP. This is merged from Config directories in the usual fashion.

Integrated JavaScript console (Mac OS X only)

Oolite debug protocol example 1.png

The integrated JavaScript console provides a window within Oolite which acts as a JavaScript console. It can be shown using the Show JavaScript Console command in the Debug menu, unless external console mode has been enabled by setting console-host in debugConfig.plist.

The integrated console has a 100-line memory. To see previous commands, press ⇞ (Page Up) or ⌥↑ (Option-Up Arrow). You can go the other way in the obvious manner.

Using the JavaScript console

The JavaScript console, whether integrated or external, provides a powerful mechanism for interactively manipulating the game world. It can be used to issue simple commands, like player.ship.awardEquipment("EQ_FUEL_INJECTION"). It can be used to inspect properties of entities in the game world. And it can be used to develop scripts, by rewriting and testing methods of live ship scripts and world scripts.

Macros

Macros are a special type of command prefixed with a colon. Instead of being interpreted as a JavaScript command, a macro is looked up in a dictionary, and the result is used as the “real” command. For instance, if you enter the command :listM (short for “list macros”), the macro dictionary is queried for the string “listM”, and (by default) the code for (let prop in macros) { ConsoleMessage('macro-list', ':' + prop) } is found and executed. This code is called the expansion of the macro. The result is a list of macros being printed:

:clr
:clear
:spawn
:bgColor
:showM
:rmFgColor
::
:d
:ds
:listM
:setM
:delM
:fgColor
:rmBgColor
:resetM

As you can see in the example, To see the expansion of a macro without executing it, use :showM:

> :showM :delM
:delM = deleteMacro(PARAM)

When a macro with the word PARAM is executed, PARAM is replaced with any text following the macro name, surrounded in quotation marks. This is how :showM functions, for example; the string “:showM :delM” is expanded to “showMacro(":delM")”, causing the JavaScript function showMacro(), which is part of the console script, to be called. You can see the implementation of showMacro() by entering showMacro – without any parentheses – into the console.

An initial set of macros is loaded from debugConfig.plist. If you edit any macros, using :setM or :delM, all macros are saved in Oolite’s preferences from that point forwards. This means that you don’t have to re-enter any macros you write with :setM after restarting Oolite.

The following example creates a simple :msg macro:

> :setM msg ConsoleMessage(' ',PARAM)

If you then type:

> :msg Hello World

The macro will be expanded, and you'll see “Hello World” on the console. However, macros are not functions, so you cannot use macros in the middle of javascript, or as part of another macro.

Console Properties

The console object has several properties. To see the full list of them check oolite-debug-console.js inside your copy of the Debug OXP.

Debug Flags

debugFlags : Number (integer, read/write)

An integer bit mask specifying various debug options. Note that the flags vary between builds. The current flags can be seen in OODebugFlags.h in the Oolite source code, for instance at: https://github.com/OoliteProject/oolite/blob/master/src/Core/Debug/OODebugFlags.h

DEBUG_LINKED_LISTS       = 0x00000001
DEBUG_COLLISIONS         = 0x00000004
DEBUG_DOCKING            = 0x00000008
DEBUG_OCTREE_LOGGING     = 0x00000010
DEBUG_BOUNDING_BOXES     = 0x00000040
DEBUG_OCTREE_DRAW        = 0x00000080
DEBUG_DRAW_NORMALS       = 0x00000100
DEBUG_NO_DUST            = 0x00000200
DEBUG_NO_SHADER_FALLBACK = 0x00000400
DEBUG_SHADER_VALIDATION  = 0x00000800
DEBUG_MISC               = 0x10000000

To set flags, you can use either numeric literals directly or symbolic names as properties of the console object. For example, to toggle rendering of bounding boxes and surface normals, you might use:

console.debugFlags ^= console.DEBUG_BOUNDING_BOXES;
console.debugFlags ^= console.DEBUG_DRAW_NORMALS;

This will work as well:

console.debugFlags ^= 0x00000040;
console.debugFlags ^= 0x00000100;


To disable everything just set it to zero:

console.debugFlags = 0

Shaders

shaderMode : String (read/write)

A string specifying the current shader mode. One of the following:

  • "SHADERS_NOT_SUPPORTED"
  • "SHADERS_OFF"
  • "SHADERS_SIMPLE"
  • "SHADERS_FULL"

If it is SHADERS_NOT_SUPPORTED, it cannot be set to any other value. If it is not SHADERS_NOT_SUPPORTED, it can be set to SHADERS_OFF, SHADERS_SIMPLE or SHADERS_FULL, unless maximumShaderMode (see below) is SHADERS_SIMPLE, in which case SHADERS_FULL is not allowed.

Note: this is equivalent to oolite.gameSettings.shaderEffectsLevel, which is available even when the debug console is not active, but is read-only.

maximumShaderMode : String (read-only)

A string specifying the fanciest available shader mode. One of the following:

  • "SHADERS_NOT_SUPPORTED"
  • "SHADERS_SIMPLE"
  • "SHADERS_FULL"
reducedDetailMode: Boolean (read/write)

Whether reduced detail mode is in effect (simplifies graphics in various ways).

glVendorString : String (read-only)
glRendererString : String (read-only)

Information about the OpenGL renderer.

Error Logging

dumpStackForErrors : Boolean (read/write)

If true, when an error or exception is reported a stack trace will be written to the log (if possible). Ignored if not showing error locations.

dumpStackForWarnings : Boolean (read/write)

If true, when a warning is reported a stack trace will be written to the log (if possible). Ignored if not showing error locations.

showErrorLocations : Boolean (read/write)

true if file and line should be shown when reporting JavaScript errors and warnings. Default: true.

showErrorLocationsDuringConsoleEval: Boolean (read/write)

Override value for showErrorLocations used while evaluating code entered in the console. Default: false. (This information is generally not useful for code passed to eval().)

Miscellaneous

displayFPS: Boolean (read/write)

Boolean specifying whether FPS (and associated information) should be displayed.

platformDescription : String (read-only)

Information about the system Oolite is running on. The format of this string is not guaranteed, do not attempt to parse it.

settings : Object

A key-value store that is saved persistently. Values from debugConfig.plist are used as defaults, and any changed values are stored with the game’s preferences.

ignoreDroppedPackets : Boolean (read/write)

If true the TCP console will try to stay connected, ignoring dropped TCP packets, if false - will disconnect if an error affects.


Profiling

Just pointing out the obvious here, but I think it needs to be noted that all these optimization techniques explained here are not themselves the objective, but the means to get performance. These techniques are not there to jump in and start writing complicated code. They should be used when there is need and where they are needed. And to find this out, the biggest weapon we have is profiling. I fully recommend that before you change anything in your code, you profile it and find out where it really needs attention.

Oolite Test Release writes something in the log header that may have escaped the attention of many, but if you look more carefully, you'll see it: It says: Build options: [...] JavaScript profiling. So you can use Oolite itself to see where bottlenecks in your OXP might be and then you can apply all these techniques exactly where there is a gain to be obtained.

. . .

Once the bottlenecks have been identified, then you know where you need to turn to and apply all the tips contained here. If you want performance, always profile.

(another_commander regarding optimization tips)

profile

function profile(func : function [, this : Object]) : String

Time the specified function, report the time spent in various Oolite functions and how much time is excluded from the time limiter mechanism. If the second argument isn't specified it defaults to the console world script.

Example of use:

console.profile(function() { return this._systemName(42) }, worldScripts["oolite-contracts-helpers"])

It will result in something like this:

Total time: 0.059 ms
JavaScript: 0.034 ms, native: 0.024 ms
Counted towards limit: 0.0439983 ms, excluded: 0.0150017 ms
Profiler overhead: 0.034 ms
                                                        NAME  T  COUNT    TOTAL     SELF  TOTAL%   SELF%  SELFMAX
               (oolite-contracts-helpers.js:161) <anonymous>  J      1     0.05     0.02    81.4    40.7     0.02
                                       SystemInfoGetProperty  N      4     0.02     0.01    28.8    20.3     0.01
                               (<console input>) <anonymous>  J      1     0.06     0.01    98.3    16.9     0.01
                                    GetJSSystemInfoForSystem  N      1     0.00     0.00     8.5     8.5     0.00
                                OOJSNativeObjectFromJSObject  N      4     0.00     0.00     6.8     6.8     0.00
                                   SystemStaticInfoForSystem  N      1     0.01     0.00    10.2     1.7     0.00
                                        OOStringFromJSString  N      4     0.00     0.00     1.7     1.7     0.00
                                           GlobalGetProperty  N      1     0.00     0.00     1.7     1.7     0.00
    -[NSString(OOJavaScriptExtensions) oo:jsValueInContext:]  N      1     0.00     0.00     0.0     0.0     0.00

The table contains the trace of the calls, with separate metrics for each one of them, as well as their type (J for Javascript, N for Native).

You may want to copy the result to the editor with a monospace font if the columns don't appear to be aligned.


Note: while profile() is running, the time limiter is effectively disabled (specifically, it's set to ten million seconds).


getProfile

function getProfile(func : function [, this : Object]) : Object

Like profile(), but returns an object, which is more amenable to processing in scripts. To see the structure of the object, run:

console.getProfile(function(){PS.position.add([0, 0, 0])})


Variables in the console

You may want to save the result of a command so that you can use it in one of the next commands. There are several ways to do this, as well as several pitfalls.

First of all, as var, let and const limit the scope of the variable to the enclosing function, they do it as well for command scope, so do not use them if you want to reuse the result. In most cases, instead of this just assign the result to the variable without declaration - it will be stored in the global object:

> a // Check to ensure that the variable is not defined
Exception: ReferenceError: a is not defined
> a = 123
123
> global.a
123
> a
123
> global.a = 456
456
> a
456

It's the simplest way, and it has one minor pitfall - the variable name must not overlap with existing global properties from the Oolite or other OXPs.

You can also use the console script's this. After defining the variable as a property of this it can be used without explicit this.. Property of this hides property of global. See the example (continues the previous):

> this.a = 789
789
> a
789
> global.a // still has the old value
456

Note that this is not a variable but keyword, and has somewhat non-obvious behaviour when used inside of functions (the example continues the previous):

> f = function() { return a } // There is no local a, so it uses one from the command's this, if it wasn't there, it will it would use one from the global scope.
> f()
789
> f = function() { return this.a }
> f() // function called not as a method, so uses "global" as "this" (only in nonstrict code)
456


Predefined and special variables

The console also offers a few short aliases by default:

P = player;
PS = player.ship;
S = system;
M = missionVariables;

They added in the this way and you can add your own - check the next section for it.


The special variable $ stores the last non-undefined result:

> system.addShipsToRoute("trader", 1, 1)
[[Ship "Boa" position: (-105926, 56511.4, 896027) scanClass: CLASS_NEUTRAL status: STATUS_IN_FLIGHT]]
> $[0]
[Ship "Boa" position: (-105926, 56511.4, 896027) scanClass: CLASS_NEUTRAL status: STATUS_IN_FLIGHT]
> $.dataKey
griff_boa_alt-NPC
> $
griff_boa_alt-NPC


The console script

While an external or integrated console is required to provide a means of user interaction, most of the actual behaviour of the console is implemented in JavaScript, in a script called oolite-debug-console.js. Input typed into the console is passed to the console script, which then generates output. The console script is also informed of log messages and JavaScript errors, and provides a global function, ConsoleMessage(colorCode: String, message: String), to write messages to the console using its background colour support. Lastly, the console script implements the entire macro system.

This means that the console’s behaviour can be extensively customized, for instance, by replacing the macro system with something more powerful. There are two basic approaches to doing this:

  • Creating a custom script (or a copy) with the same name, in an OXP which loads later than Debug.oxp (e.g., one which has a name which comes later in alphabetical order).
  • Using a normal world script, and changing the console script object at startup.

The latter approach has the advantage that it can continue to work with future versions of the console script, and multiple patches can coexist. For example, a modification to supplement the macro system with a better one might look as follows:

this.name = "my-super-console-macros"

this.startUp = function()
{
    // Ensure debug console is installed
    if (!console)  return
    
    // Save the original implementation of consolePerformJSCommand
    // Note the prefix, used to ensure uniqueness. If several scripts patch
    // the same method, it's important that they use different names.
    console.script.mySuperConsoleMacros_original_consolePerformJSCommand = console.script.consolePerformJSCommand
    
    // Replace console.script.consolePerformJSCommand with custom function
    console.script.consolePerformJSCommand = function(command)
    {
        // Note that in this function "this" will refer to console.script, "oolite-debug-console", not
        // "my-super-console-macros", since the function will be called as a
        // method of the console.
        
        // Strip leading spaces, same code as the original function.
        while (command.charAt(0) == " ")
        {
            command = command.substring(1)
        }
        
        if (command.charAt(0) == "!")
        {
            // Super macro call detected.
            // Insert super macro system here.
        }
        else
        {
            // Otherwise, call through to the original method.
            console.script.mySuperConsoleMacros_original_consolePerformJSCommand(command)
        }
    }
}

This same patching technique, incidentally, can be used to modify scripts at runtime, either from the console or from other scripts.

Links