On Watch
by Neil C Smith
PraxisLIVE v6.1 saw the first introduction of Watch functions into PraxisCORE, along with basic UI support in the PraxisLIVE graph editor. These saw further enhancement in the v6.2 release, and still more is planned.
A Watch function is a function control for accessing information about a component. They are somewhat similar to read-only properties, but for data that should only be calculated on demand, and perhaps asynchronously. They also support extended metadata, and the option of query arguments to request specific information. Watch functions are somewhat analogous to watch expressions in a debugger, but within the context of an active real-time re-codeable system. They can be rewritten as necessary to explore and expose the state of the running system, while having minimal overhead when not in use.
The only required metadata for a Watch function is the media (MIME) type of the returned data, which is used to select a suitable viewer. The function should also respond with the data type specified by the media type - usually String for text-based formats, and PBytes for binary ones. The graph component inside PraxisLIVE currently provides viewers for PNG and SVG images, as well as plain text.
Defining Watches
Watch functions can be defined in code using the @FN.Watch
annotation. This works
similarly to the generic @FN
annotation (and under the hood uses the same control
binding), with additional fields for defining the necessary metadata.
A simple text watch could be defined using -
@FN.Watch(mime = MIME_TEXT)
public String text() {
return "Hello Watcher!";
}
- although as this returns the same text each time, it’s not the most useful of
watches! The watch can be exposed on the graph editor in the IDE using the Expose
sub-menu in the component popup. The expose feature also supports other types of
control, such as properties. To expose any watch or control by default, use the
@Config.Expose
annotation.
Another supported media type for watches is SVG. The SVG data should be returned from the control as a String, and in a format suitable for embedding in HTML5 (the IDE doesn’t use HTML, but other future contexts will). We can write a component that highlights the state of a boolean property, and relate the watch to the port, using -
@P boolean status;
@Config.Expose("watch")
@Override
public void init() {
}
@FN.Watch(mime = MIME_SVG, relatedPort = "status")
String watch() {
return """
<svg width="64" height="64">
<circle cx="32" cy="32" r="32" fill="COLOUR" />
</svg>
""".replace("COLOUR", status ? "green" : "red");
}
Async responses
Also added in v6.1 was support for asynchronous returns from function controls,
which are very useful for Watches. Asynchronous values in component code are
represented using the Async<T>
type. An Async is somewhat similar to a standard
Java Future, however it is neither thread-safe nor blocking. It is not possible
to wait on the value, which will be completed by a future actor call. Function
response calls will automatically be generated and sent when the returned Async
completes.
We might use the standard ask()
function to send a call to another control and
return its result, or use async()
to generate a response, or manage
internally using Async
or Async.Queue
fields.
In the code below we create a video:custom
component that can be used to watch
video frames moving through the pipeline. This is a reduced version of the code in
the built-in video:composite
. In the watch function itself we return an Async<PBytes>
that has been wrapped by the timeout function - this will complete the Async with
error if nothing else does so within the specified time. We cannot initiate the
encoding of the input itself until draw()
is called. During the drawing method,
a check is made to see if a watch result is pending. A scaled down PNG encoding of
the input is started using write()
which also returns an Async value. The pending
Async is bound to the PNG result and then the field cleared.
@In PImage in;
@Persist Async<PBytes> watchInResponse;
@Override
public void draw() {
if (watchInResponse != null) {
double scale = 0.25;
Async<PBytes> png = write(MIME_PNG, in, scale);
Async.bind(png, watchInResponse);
watchInResponse = null;
}
copy(in);
release(in);
}
@FN.Watch(mime = MIME_PNG, relatedPort = "in")
Async<PBytes> watchIn() {
if (watchInResponse == null || watchInResponse.failed()) {
watchInResponse = timeout(1, new Async<>());
}
return watchInResponse;
}
Chaining, queries and future moldability
Watch functions can accept optional or required queries in the form of maps, although this feature is not yet exposed in the IDE interface.
For example, the first text example might be changed to -
@FN.Watch(mime = MIME_TEXT)
public String text(PMap query) {
return "Hello " + query.getString("name", "<UNKNOWN>");
}
The Output window in the IDE can be used to call this function -
> /root/component.text [map name John]
--- Hello John
Queries will be used in future to support a range of feature such as data ranges and other formats.
Another future plan is to support chaining watches, using re-codeable watches in other components or embedded within the IDE to transform data, to highlight certain features or adapt media types. Watches are also designed to be used in other contexts away from the PraxisLIVE IDE, such as in a HTML-based UI or notebook-like interface.
All in all, Watches provide an open-ended way to explore, understand and represent what is going on inside your projects as they are running.