A little Morphine Tutorial
Morphine is a trace analysis system of Mercury
programs.
The goal of this tutorial is to give you an rough idea about what Morphine
is. If you want a more exhaustive presentation, please refer to the
Getting
started and A debugging session with Morphine
sections of the Morphine User Manual.
This tutorial consists in a Morphine trace analysis session of a Mercury
version of the program that solves the famous N-queens problem (queens.m).
-
Running Morphine
-
Morphine = Prolog + current/1
+ next/0
-
Toward an efficient extensible debugger: fget/1
-
Toward efficient extensible monitors:
collect/2 (also called foldt/2)
-
If you want to know more about Morphine...
Running Morphine
-
First of all, the Mercury program (queens.m) has to be compiled with debugging
options:
$ mmc --trace deep queens.m
This creates the executable program `queens'.
Now, let's run Morphine!
$ morphine
ECLiPSe Constraint Logic Programming System [kernel]
Version 4.1.0, Copyright IC-Parc and ICL, Sun Feb 21 18:37 1999
[morphine]:
Morphine is nothing more than an Eclipse interpreter in which Morphine
modules have been loaded.
-
The first thing to do is to run the Mercury program under the control of
Morphine. To do that, we use the command run/1.
[morphine]: run(queens). Start
debugging queens program.
1: 1 [1] call
main(state('<c_pointer>'), -)
[morphine]:
The two process, Morphine and the
debugged program, are now running in coroutining. The execution stops
at the first event and gives the control to Morphine which prints a
trace line and waits for user queries. The trace line here simply
says that the current event is the first one, it is the first goal
called, the execution depth is 1, it is a call event and the current
predicate is main/2. state('<c_pointer>') means
that the first argument of the current predicate is a I/O state (C
pointer); the '-' stands for non instantiated
arguments.
Only two primitives plus Prolog are enough to build a powerful trace analysis
system. In this section, we are not concerned with efficiency; we will
deal with efficiency issues in the next two sections. Let me first quickly
show what those two primitives `current/1'
and `next/0' are.
-
The predicate `current/1' lets users retrieve any information
relative to the current event. Each elementary piece of information
is called an event attribute. Examples of event attributes are the
event number, the execution number, the port, the predicate name, the
list of its argument, etc. The whole list of event attributes (and
their aliases) can be obtained via the command
`list_attribute_aliases/0' (or `laa/0', for
short).
For example, in order to retrieve the current port, the Morphine query...
[morphine]: current(port = Port).
Port = call
More? (;)
... unifies the logical variable Port with the port attribute
of the current event.
-
The second primitive we need is the one to make the execution move
one event forward. This primitive is called `next/0';
`next/0' makes the execution proceeds until the next
execution event is reached and then prints a trace line (by
calling the `print_event/0' predicate).
[morphine]: next. 2: 2 [2]
call data(-)
-
For example, in order to dump the whole execution trace, we can take
advantage of the repeat-fail mechanism of Prolog and type the
following query:
[morphine]: next, fail. 3: 2
[2] exit data([1, 2, 3, 4, 5])
4: 3 [2] call queen([1,
2, 3, 4, 5], -)
5: 4 [3] call qperm([1, 2, 3, 4, 5],
-)
6: 4 [3] switch qperm([1, 2, 3, 4, 5], -)
7: 5 [4] call qdelete(-, [1, 2, 3, 4, 5], -)
8:
5 [4] disj qdelete(-, [1, 2, 3, 4, 5], -)
9: 5 [4] exit
qdelete(1, [1, 2, 3, 4, 5], [2, 3, 4, 5])
10: 6 [4] call
qperm([2, 3, 4, 5], -)
11: 6 [4] switch qperm([2, 3, 4,
5], -)
...
-
Now, we can combine those commands in order to move to given points
of the execution trace. But before, we run again the lastly run
program (rerun/0):
[morphine]: rerun. Start
debugging queens program.
1: 1 [1] call
main(state('<<c_pointer>>'), -)
[morphine]: next_np, current(name = qperm and port = exit),
print_event. 32: 14 [8] exit qperm([],
[])
That query makes the execution move until the first success of
predicate `qperm', and then prints a trace line. The
predicate `next_np/0' is the same one as `next/0',
except it does not print a trace line (i.e., it does not call
`print_event/0'; "_np" stands for "no print").
-
With those two primitives, we can also write (trace analysis)
programs. For example, here is how to implement a simple monitor
that count the number of goal calls.
[morphine]: [user].
count_call(N) :-
( next_np ->
( current(port = call) -> N1 is N + 1 ; N1 is N),
count_call(N1)
;
% the end of the execution is reached
printf("The number of goals called is %w.\n", N)
).
^D
Then, we can use that new command to count the number
of calls in the queens program:
[morphine]: rerun,
count_call(1). Start debugging queens
program.
1: 1 [1] call
main(state('<<c_pointer>>'), -)
[1, 3, 5, 2,
4]
last event is reached
End of connection
with the traced program
The number of goals called is
146.
Toward an efficient extensible debugger: fget/1
The problem with the previously described trace
analysis system is the efficiency. Each time you need to move one
event forward in the execution, you send a message to the debuggee
process, receive a response from it, and then interpret it. This
message exchange together with the Operating System level context
switches it induces can cost a lot, especially to process a million
of execution events, which is quite common. In order to avoid those inter process communications,
we introduced a new primitive, called `fget/1', that filters out execution
events in the debuggee process. Actually, fget(ListOf Attributes) does exactly the same job as the sequence of goals
next_np, current(ListOfAttributes)
except it requires only one
communication between Morphine and the debuggee (instead of 32, as it
required in the previous example).
-
Hence, we can move to the first success of `qperm'
like this:
[morphine]: rerun, fget(name = qperm and port = exit).
Start debugging queens program.
1: 1 [1] call main(state('<<c_pointer>>'), -)
32: 14 [8] exit
qperm([], [])
-
Using fget/1 and current/1, you can implement all classical
debugging commands as efficiently as their hard coded counterparts
(with an handful of communications between the two processes).
-
The count_call/1 monitor can now be implemented using fget
like this:
[morphine]: [user].
count_call(N) :-
( fget_np(port = call)
->
N1 is N + 1,
count_call(N1)
;
% the end of the execution is reached
printf("The number of goal call is %w.\n", N)
).
^D
-
The purpose of fget /1 is really the efficiency, but note that
it also makes Morphine queries and programs more compact.
Toward
efficient extensible monitors: collect/2
(also called foldt/2)
The primitives fget/1 and current/1 are enough to
efficiently implement most debugging commands. But when it comes to
write monitors (a monitor is a program that analyses and extracts run
time information about a whole program execution, like the count_call/1
program we have just given) then it turns out that it is not
satisfactory. For example, the fget/1 version of the
monitor that counts the number of calls may require about 1/5
of the communications the next/0 version needs; but if we
want to monitor a 5 million events execution, this means that it
still requires about one million message exchanges. The idea is that
for monitoring a whole program execution, it is not necessary to
interact with the program as in debugging, and thus you do not want
to pay the price induced by the coroutining. With collect/2, we will be
able to do it in one pass. - The command collect/2
is a kind of foldl/4 meta-predicate that operates on a
list of events and which is directly plugged into the Mercury trace
system. As with foldl/4, before calling collect/2
you need to initialize an accumulator (initialize/1) and to
define a filter (filter/4) that will be applied to each
element of the list of events and to the accumulator;
collect/2 outputs the final value of the accumulator in its
second argument. The initialization and the filtering predicates are
defined in Mercury.
-
For example, to implement the monitor that counts the number of goals called
using collect/2, you just need to write the following code in
a file named, let's say, count_call:
"
:- import_module int.
:- type collected_type == int.
initialize(0).
filter(Event, Cpt0, Cpt, continue) :-
( if port(Event) = call then Cpt
= Cpt0+1 else Cpt = Cpt0 ).
"
Then, you just need to call collect/2 with the name of that file
as its first argument; the result of the monitoring activity will be bounded
with its second argument.
[morphine]: rerun, collect(count_call, Result).
Start debugging queens program.
1: 1 [1] call main(state('<<c_pointer>>'), -)
[1, 3, 5, 2, 4]
End of connection with the traced program
Result = 146
Actually, the count_call file is used to generated a Mercury module
that is compiled, dynamically linked with the current Mercury program execution.
Then the resulting program is executed. When the end of the execution is
reached, the result is send to Morphine.
-
For more details about collect/2, you might want to have a look
at the
TPLP 2002
paper which full ref is given on my web page.
If you want to know more about Morphine...
You can have a look at the HTML version the Morphine
User and Reference manual. You can also play with the on-line help
features, i.e., using the `man/1' and the
`apropos/1' commands.
[morphine]: man apropos.
apropos(Name) {a}
Command which displays all the commands, primitives, procedures,
parameters, or
types for which Name is a substring of.
Example:
[morphine]: apropos man.
man
manual
latex_manual
window_command
opium_command_in_module
print_man
Name : atom
type of command : opium