SWISH - slow printing and missing toplevel bindings

First, thank you for SWI Prolog and SWISH. I work in a restricted environment, so being able to run Prolog code without installing software is a huge benefit.

I am using portray_clause to generate a list of facts that my (non-programmer) users will copy and paste into a cell of a SWISH notebook. I notice that predicates like format and portray_clause are quite slow, to the point where simple queries like the following have a visible delay when printing:

format("~w", [a]), format("~w", [b]), format("~w", [c]).
maplist(portray_clause, [a, b, c, d]).

Is this expected? Is there anything I can do to speed up printing?

I also notice that the results of toplevel bindings do not seem to be available in subsequent queries. E.g. if I write X = 2. in one query, then write($X). in a subsequent query, I get an unbound variable instead of 2. Is this a limitation of SWISH? I was hoping to persist a query result between cells.

As is, yes. Writing is based on “long polling”, so a write causes an HTTP request from the client to complete, after which the client adds the output to the browser and issues a new polling request.

What you can do is use with_output_to/2 to write all your output to a string and then write the string as a whole.

SWISH is stateless. Pretty much by design. That guarantees reproducible results. It is certainly possible to change that as the underlying Pengine (Prolog engine) protocol can deal with using the same engine for subsequent calls. It is not on my agenda. Of course, we can deliver these things as commercial addition.

If you want a stateful Prolog engine on the wen, the WASM version might be an alternative to you. Both have (dis)advantages.

Thank you Jan, with_output_to/2 works well for my needs.

I think stateless is probably better for my users anyway.

Would it be possible to change the implementation to use
a web socket and buffering. Actually I don’t know at the moment
how you can deal with 100 web socket connections. I guess

one approach would be JavaScript async programming, so instead of
having 100 threads serving 100 web sockets, one would only
have 1 thread serving 100 web sockets server side quasi parallel.

Didn’t try yet the web socket thing. On the other hand I think
buffering in Prolog is well established. You wouldn’t need to call
with_output_to/2, but could directly use write/1, format/1, etc…

as known from standalone, all you would need is to call either
nl/1 or flush_output/0. The convention could be that nl/1 does
an auto-flush. It puts a little bit more memory pressure on the

server, since each Prolog execution would also have an
output buffer, like 8192 bytes or something.

Edit 02.06.2024
There is a slight danger with buffering. Namely the end-user
might forget the flush_output/0 call. This might be the reason
that some Prolog systems have auto-flush for write/1, print/1, etc…

as well. If I am not mistaken SWI-Prolog has even an advanced logic
in standalone that looks whether a prompt was generated, i.e. if the
cursor is away from the left margin. An alternative cheaper solution

is to flush the buffer when chunk of work was done. For example in the
processing of a notebook cell. You have the opportunity to perform
an additional flush, which doesn’t hurt since flush is idempodent.

OMG this is getting extremely technical…the programmer’s playground really…I like it although of course I understand close to 0…but it sounds really serious :+1:t2:

OMG @Marco has never seen the heart symbol in discourse. Instead
of stalking me thats typically the easier way to express your love for my
contributions to SWI-Prolog discourse and it generates less noise.

Actually there is no harm in hard coding an auto-flush like this:

% nl(+Stream)
nl(Stream) :-
   put_code(Stream, 0'\n),

If the end-user wants a new line without auto-flush he can still
call put_code(0'\n). So my slightly “ugly” feeling about it changed,
I rather have the impression now that it is “beautiful”. Provided

0'\n is understood by the output device as a newline, in this
case the output device is a HTTP client even. If this is not
ensured one has to use a more complicated solution.

That would be an option. The system has library(http/hub) that handles I/O to many websockets using the select() or poll() API to keep track of readiness of many sockets. Not sure how much it would help though. After all, quickly repeating output will use HTTP Keep-Alive and thus keep using the same connection. We do need a full round trip for each message using polling though, while we would not need confirmation when using a websocket for printing. An advantage of polling is that it is supported everywhere. Notably in between proxies can be problematic. Although websockets seem to have quite large support now.

I don’t think I’m a fan of full buffering, mostly for the reasons you give.

(Nothing of importance)

The SWISH output mechanism is a little complicated. The client waits for the Prolog answer (after sending the query). It may get the answer, or a “prompt” response that contains something to write as well as an indication of which input the server expects. That is, for example, use by the tracer. This writes the trace output and expects a new HTTP request that that provides the answer to the input request and -again- waits for a Prolog answer or a new prompt.

Most output predicates have been redefined, translating the request in an HTML fragment that is added to the SWISH console. Notably term-writing creates HTML that provides style as well as folding, layout, etc.

Finally, the output stream is redefined to capture output that does not go through the SWISH I/O redefinition. The stream runs in line buffering mode and generates an HTML element for each line (and sends it though the above prompt mechanism). I guess that ideally this should combine multiple lines into a single reply if lines are being sent in high frequency. Not sure how to deal with that as we need to take action if we do not get a new output requests within a few milliseconds or we connected enough output to make sending worthwhile. It is worth considering as it would speedup SWISH output over networks with long latency a lot.

I have pushed an extension to the Pengines library that allows combining multiple events that require no input into a single event if they are generated within a specified time. That currently combines multiple output events, where SWISH configures this to wait for 10ms for a new event and collect them in batches of at most 100 events.

This greatly speeds up SWISH programs that use multiple write/format/… calls, reducing bandwidth requirements at the same time.

The patch also adds support for put_code/1 and put_char/1.

1 Like