swish/commit

Copied files

authorJan Wielemaker
Fri Mar 4 17:30:54 2016 +0100
committerJan Wielemaker
Fri Mar 4 17:30:54 2016 +0100
commitd0e7313128d750ac32f781d54da304a2cae8c833
tree4278ab625c9e9e8874f64a02ed95e4a01af35225
parent89da5b1e8d57456217ca09b655fa6881b945de0b
Diff style: patch stat
diff --git a/lib/swish/config.pl b/lib/swish/config.pl
index 59a1632..9ca6978 100644
--- a/lib/swish/config.pl
+++ b/lib/swish/config.pl
@@ -28,9 +28,9 @@
 */
 
 :- module(swish_config,
-	  [ swish_reply_config/1,	% +Request
+	  [ swish_reply_config/2,	% +Request, +Options
 	    swish_config/2,		% ?Type, ?Config
-	    swish_config_hash/1		% -HASH
+	    swish_config_hash/2		% -HASH, +Options
 	  ]).
 :- use_module(library(http/http_dispatch)).
 :- use_module(library(http/http_json)).
@@ -38,6 +38,7 @@
 
 :- multifile
 	config/2,			% ?Key, ?Value
+	config/3,			% ?Key, ?Value, +Options
 	source_alias/2,			% ?Alias, ?Options
 	authenticate/2.			% +Request, -User
 
@@ -48,31 +49,31 @@
 		 *	       CONFIG		*
 		 *******************************/
 
-%%	swish_reply_config(+Request) is semidet.
+%%	swish_reply_config(+Request, +Options) is semidet.
 %
 %	Emit a configuration object to the client if the client requests
 %	for '.../swish_config.json', regardless  of   the  path  prefix.
 
-swish_reply_config(Request) :-
+swish_reply_config(Request, Options) :-
 	option(path(Path), Request),
 	file_base_name(Path, 'swish_config.json'),
-	json_config(JSON),
+	json_config(JSON, Options),
 	reply_json(JSON).
 
-%%	swish_config_hash(-Hash) is det.
+%%	swish_config_hash(-Hash, +Options) is det.
 %
 %	True if Hash is the SHA1 of the SWISH config.
 
-swish_config_hash(Hash) :-
-	json_config(Config),
+swish_config_hash(Hash, Options) :-
+	json_config(Config, Options),
 	variant_sha1(Config, Hash).
 
 json_config(json{ http: json{ locations:JSON
 			    },
 		  swish: SWISHConfig
-		}) :-
+		}, Options) :-
 	http_locations(JSON),
-	swish_config(SWISHConfig).
+	swish_config_dict(SWISHConfig, Options).
 
 http_locations(JSON) :-
 	findall(ID-Path,
@@ -101,12 +102,12 @@ same_ids([Id-Path|T0], Id, T, [Path|TP]) :- !,
 same_ids(T, _, T, []).
 
 
-%%	swish_config(-Config:dict) is det.
+%%	swish_config_dict(-Config:dict, +Options) is det.
 %
 %	Obtain name-value pairs from swish_config:config/2
 
-swish_config(Config) :-
-	findall(Key-Value, config(Key, Value), Pairs),
+swish_config_dict(Config, Options) :-
+	findall(Key-Value, swish_config(Key, Value, Options), Pairs),
 	dict_pairs(Config, json, Pairs).
 
 %%	config(-Key, -Value) is nondet.
@@ -116,6 +117,11 @@ swish_config(Config) :-
 %	object (see =web/js/config.js=)
 
 swish_config(Key, Value) :-
+	swish_config(Key, Value, []).
+
+swish_config(Key, Value, Options) :-
+	config(Key, Value, Options).
+swish_config(Key, Value, _) :-
 	config(Key, Value).
 
 %%	source_alias(?Alias, ?Options) is nondet.
diff --git a/lib/swish/highlight.pl b/lib/swish/highlight.pl
index 57412c2..0775725 100644
--- a/lib/swish/highlight.pl
+++ b/lib/swish/highlight.pl
@@ -690,6 +690,7 @@ style(meta(_Spec),	 meta,				   []).
 style(op_type(_Type),	 op_type,			   [text]).
 style(functor,		 functor,			   [text]).
 style(control,		 control,			   [text]).
+style(delimiter,	 delimiter,			   [text]).
 style(identifier,	 identifier,			   [text]).
 style(module(_Module),   module,			   [text]).
 style(error,		 error,				   [text]).
@@ -740,21 +741,21 @@ neck_text(method(send), (:->)).
 neck_text(method(get),  (:<-)).
 neck_text(directive,    (:-)).
 
-head_type(exported,	head_exported).
-head_type(public(_),	head_public).
-head_type(extern(_),	head_extern).
-head_type(dynamic,	head_dynamic).
-head_type(multifile,	head_multifile).
-head_type(unreferenced,	head_unreferenced).
-head_type(hook,		head_hook).
-head_type(meta,		head_meta).
-head_type(constraint,	head_constraint).
-head_type(imported,	head_imported).
-head_type(built_in,	head_built_in).
-head_type(iso,		head_iso).
-head_type(def_iso,	head_def_iso).
-head_type(def_swi,	head_def_swi).
-head_type(_,		head).
+head_type(exported,	 head_exported).
+head_type(public(_),	 head_public).
+head_type(extern(_),	 head_extern).
+head_type(dynamic,	 head_dynamic).
+head_type(multifile,	 head_multifile).
+head_type(unreferenced,	 head_unreferenced).
+head_type(hook,		 head_hook).
+head_type(meta,		 head_meta).
+head_type(constraint(_), head_constraint).
+head_type(imported,	 head_imported).
+head_type(built_in,	 head_built_in).
+head_type(iso,		 head_iso).
+head_type(def_iso,	 head_def_iso).
+head_type(def_swi,	 head_def_swi).
+head_type(_,		 head).
 
 goal_type(built_in,	      goal_built_in,	 []).
 goal_type(imported(File),     goal_imported,	 [file(File)]).
diff --git a/lib/swish/include.pl b/lib/swish/include.pl
index c6a0d8c..39eb41a 100644
--- a/lib/swish/include.pl
+++ b/lib/swish/include.pl
@@ -60,11 +60,13 @@ swish:term_expansion(:- include(FileIn), Expansion) :-
 		          'swish included'(File),
 		          (:- include(stream(Id, Stream, [close(true)])))
 			],
+	    '$push_input_context'(swish_include),
 	    setting(web_storage:directory, Store),
 	    add_extension(File, FileExt),
 	    catch(gitty_data(Store, FileExt, Data, _Meta), _, fail),
 	    atom_concat('swish://', FileExt, Id),
-	    open_string(Data, Stream)
+	    open_string(Data, Stream),
+	    '$pop_input_context'
 	).
 
 add_extension(File, FileExt) :-
diff --git a/lib/swish/logging.pl b/lib/swish/logging.pl
index 51e2183..55339aa 100644
--- a/lib/swish/logging.pl
+++ b/lib/swish/logging.pl
@@ -64,13 +64,32 @@ swish_log(send(Pengine, Event)) :-
 		 [HDate, Now, send(Pengine, Event)]).
 
 :- dynamic
-	text_hash/2.
+	text_hash/3,
+	gc_text_hash_time/1.
 
 hash_option(src_text(Text), src_text(Result)) :- !,
-	(   text_hash(Text, Hash)
+	(   text_hash(Text, _, Hash)
 	->  Result = Hash
 	;   variant_sha1(Text, Hash),
-	    assert(text_hash(Text, Hash)),
+	    get_time(Now),
+	    assert(text_hash(Text, Now, Hash)),
+	    gc_text_hash,
 	    Result = Hash-Text
 	).
 hash_option(Option, Option).
+
+gc_text_hash :-
+	gc_text_hash_time(Last),
+	get_time(Now),
+	Now - Last < 900, !.
+gc_text_hash :-
+	get_time(Now),
+	retractall(gc_text_hash_time(_)),
+	asserta(gc_text_hash_time(Now)),
+	Before is Now - 3600,
+	(   text_hash(Text, Time, Hash),
+	    Time < Before,
+	    retractall(text_hash(Text, Time, Hash)),
+	    fail
+	;   true
+	).
diff --git a/lib/swish/page.pl b/lib/swish/page.pl
index e800d93..0778187 100644
--- a/lib/swish/page.pl
+++ b/lib/swish/page.pl
@@ -96,18 +96,21 @@ http:location(pldoc, swish(pldoc), [priority(100)]).
 %	  - q(Query)
 %	  Use Query as the initial query.
 
-swish_reply(_Options, Request) :-
-	swish_config:authenticate(Request, _User), % must throw to deny access
-	fail.
 swish_reply(Options, Request) :-
+	swish_config:authenticate(Request, User), !, % must throw to deny access
+	swish_reply2([user(User)|Options], Request).
+swish_reply(Options, Request) :-
+	swish_reply2(Options, Request).
+
+swish_reply2(Options, Request) :-
 	option(method(Method), Request),
 	Method \== get, !,
 	swish_rest_reply(Method, Request, Options).
-swish_reply(_, Request) :-
+swish_reply2(_, Request) :-
 	serve_resource(Request), !.
-swish_reply(_, Request) :-
-	swish_reply_config(Request), !.
-swish_reply(SwishOptions, Request) :-
+swish_reply2(Options, Request) :-
+	swish_reply_config(Request, Options), !.
+swish_reply2(SwishOptions, Request) :-
 	Params = [ code(_,	 [optional(true)]),
 		   background(_, [optional(true)]),
 		   examples(_,   [optional(true)]),
@@ -119,19 +122,19 @@ swish_reply(SwishOptions, Request) :-
 	merge_options(Options0, SwishOptions, Options1),
 	source_option(Request, Options1, Options2),
 	option(format(Format), Options2),
-	swish_reply2(Format, Options2).
+	swish_reply3(Format, Options2).
 
-swish_reply2(raw, Options) :-
+swish_reply3(raw, Options) :-
 	option(code(Code), Options), !,
 	format('Content-type: text/x-prolog~n~n'),
 	format('~s', [Code]).
-swish_reply2(json, Options) :-
+swish_reply3(json, Options) :-
 	option(code(Code), Options), !,
 	option(meta(Meta), Options, _{}),
 	reply_json_dict(json{data:Code, meta:Meta}).
-swish_reply2(_, Options) :-
+swish_reply3(_, Options) :-
 	swish_config:reply_page(Options), !.
-swish_reply2(_, Options) :-
+swish_reply3(_, Options) :-
 	reply_html_page(
 	    swish(main),
 	    [ title('SWISH -- SWI-Prolog for SHaring'),
@@ -352,7 +355,7 @@ swish_content(Options) -->
 	{ document_type(Type, Options)
 	},
 	swish_resources,
-	swish_config_hash,
+	swish_config_hash(Options),
 	html(div([id(content), class([container, swish])],
 		 [ div([class([tile, horizontal]), 'data-split'('50%')],
 		       [ div([ class([editors, tabbed])
@@ -370,14 +373,14 @@ swish_content(Options) -->
 		 ])).
 
 
-%%	swish_config_hash//
+%%	swish_config_hash(+Options)//
 %
 %	Set `window.swish.config_hash` to a  hash   that  represents the
 %	current configuration. This is used by   config.js  to cache the
 %	configuration in the browser's local store.
 
-swish_config_hash -->
-	{ swish_config_hash(Hash) },
+swish_config_hash(Options) -->
+	{ swish_config_hash(Hash, Options) },
 	js_script({|javascript(Hash)||
 		   window.swish = window.swish||{};
 		   window.swish.config_hash = Hash;
diff --git a/lib/swish/procps.pl b/lib/swish/procps.pl
index e31e824..61aff16 100644
--- a/lib/swish/procps.pl
+++ b/lib/swish/procps.pl
@@ -93,6 +93,8 @@ stat_field_value(_, String, Number) :-
 	number_string(Number, String).
 
 :- if(current_predicate(sysconf/1)).
+% the weird way to call sysconf confuses ClioPatria's cpack code
+% analysis enough to accept this ...
 term_expansion(clockticks(sysconf), Expansion) :-
 	(   member(Sysconf, [sysconf(clk_tck(TicksPerSec))]),
 	    call(Sysconf)
diff --git a/lib/swish/render/c3.pl b/lib/swish/render/c3.pl
index 069a340..b90fd57 100644
--- a/lib/swish/render/c3.pl
+++ b/lib/swish/render/c3.pl
@@ -54,6 +54,10 @@ Render data as a chart.
 %	Renders Term as a C3.js chart. This renderer recognises C3, as a
 %	dict with tag `c3`.
 
+% This renderer hooks into SWISH using two event-handlers and associated
+% classes. The `reactive-size` is called  after   the  window or pane is
+% resized and the `export-dom` is called from the _download_ button.
+
 term_rendering(C30, _Vars, _Options) -->
 	{ is_dict(C30, Tag),
 	  Tag == c3,
@@ -62,7 +66,7 @@ term_rendering(C30, _Vars, _Options) -->
 	  atom_concat(#, Id, RefId),
 	  put_dict(bindto, C3, RefId, C3b)
 	},
-	html(div([ class(['render-C3', 'reactive-size']),
+	html(div([ class(['render-C3', 'reactive-size', 'export-dom']),
 		   'data-render'('As C3 chart')
 		 ],
 		 [ div(id(Id), []),
@@ -75,6 +79,17 @@ term_rendering(C30, _Vars, _Options) -->
     var sizing = {};
     var tmo;
 
+    div.on("export-dom", function(ev, r) {
+      var svg = div.find("svg");
+      svg.attr("xmlns", "http://www.w3.org/2000/svg");
+      svg.css("font", "10px sans-serif");
+      svg.find(".c3-path,.c3-line,.tick,.domain").css("fill", "none");
+      svg.find(".tick,.domain").css("stroke", "#000");
+      r.element = svg[0];
+      r.extension = "svg";
+      r.contentType = "image/svg+xml";
+    });
+
     div.on("reactive-resize", function() {
       if ( chart ) {
 	if ( tmo ) clearTimeout(tmo);
diff --git a/lib/swish/render/table.pl b/lib/swish/render/table.pl
index d11962e..400c852 100644
--- a/lib/swish/render/table.pl
+++ b/lib/swish/render/table.pl
@@ -32,6 +32,8 @@
 	  ]).
 :- use_module(library(apply)).
 :- use_module(library(lists)).
+:- use_module(library(pairs)).
+:- use_module(library(dicts)).
 :- use_module(library(option)).
 :- use_module(library(http/html_write)).
 :- use_module(library(http/term_html)).
@@ -54,6 +56,17 @@ Render table-like data.
 %
 %	@tbd: recognise more formats
 
+term_rendering(Term, _Vars, Options) -->
+	{ is_list_of_dicts(Term, _Rows, ColNames)
+	}, !,
+	html(div([ style('display:inline-block'),
+		   'data-render'('List of terms as a table')
+		 ],
+		 [ table(class('render-table'),
+			 [ \header_row(ColNames),
+			   \rows(Term, Options)
+			 ])
+		 ])).
 term_rendering(Term, _Vars, Options) -->
 	{ is_list_of_terms(Term, _Rows, _Cols),
 	  header(Term, Header, Options)
@@ -63,7 +76,7 @@ term_rendering(Term, _Vars, Options) -->
 		 ],
 		 [ table(class('render-table'),
 			 [ \header_row(Header),
-			   \rows(Term)
+			   \rows(Term, Options)
 			 ])
 		 ])).
 term_rendering(Term, _Vars, Options) -->
@@ -75,24 +88,31 @@ term_rendering(Term, _Vars, Options) -->
 		 ],
 		 [ table(class('render-table'),
 			 [ \header_row(Header),
-			   \rows(Term)
+			   \rows(Term, Options)
 			 ])
 		 ])).
 
-rows([]) --> [].
-rows([H|T]) -->
+rows([], _) --> [].
+rows([H|T], Options) -->
 	{ cells(H, Cells) },
-	html(tr(\row(Cells))),
-	rows(T).
-
-row([]) --> [].
-row([H|T]) -->
+	html(tr(\row(Cells, Options))),
+	rows(T, Options).
+
+row([], _) --> [].
+row([H|T], Options) -->
+	html(td(\term(H, Options))),
+	row(T, Options).
+row([H|T], Options) -->
 	html(td(\term(H, []))),
-	row(T).
+	row(T, Options).
 
 cells(Row, Cells) :-
 	is_list(Row), !,
 	Cells = Row.
+cells(Row, Cells) :-
+	is_dict(Row), !,
+	dict_pairs(Row, _Tag, Pairs),
+	pairs_values(Pairs, Cells).
 cells(Row, Cells) :-
 	compound(Row),
 	compound_name_arguments(Row, _, Cells).
@@ -156,6 +176,20 @@ is_term_row(Name, Arity, Term) :-
 	compound(Term),
 	compound_name_arity(Term, Name, Arity).
 
+%%	is_list_of_dicts(@Term, -Rows, -ColNames) is semidet.
+%
+%	True when Term is a list of Rows dicts, each holding ColNames as
+%	keys.
+
+is_list_of_dicts(Term, Rows, ColNames) :-
+	is_list(Term), Term \== [],
+	length(Term, Rows),
+	maplist(is_dict_row(ColNames), Term).
+
+is_dict_row(ColNames, Dict) :-
+	is_dict(Dict),
+	dict_keys(Dict, ColNames).
+
 %%	is_list_of_lists(@Term, -Rows, -Cols) is semidet.
 %
 %	Recognise a list of lists of equal length.
diff --git a/lib/swish/storage.pl b/lib/swish/storage.pl
index da7dcbc..5a9f6a1 100644
--- a/lib/swish/storage.pl
+++ b/lib/swish/storage.pl
@@ -84,6 +84,10 @@ web_storage(Request) :-
 	storage(Method, Request).
 
 storage(get, Request) :-
+	(   swish_config:authenticate(Request, User)
+	->  Options = [user(User)]
+	;   Options = []
+	),
 	http_parameters(Request,
 			[ format(Fmt,  [ oneof([swish,raw,json,history,diff]),
 					 default(swish),
@@ -106,7 +110,8 @@ storage(get, Request) :-
 	->  Format = diff(RelTo)
 	;   Format = Fmt
 	),
-	storage_get(Request, Format).
+	storage_get(Request, Format, Options).
+
 storage(post, Request) :-
 	http_read_json_dict(Request, Dict),
 	option(data(Data), Dict, ""),
@@ -220,7 +225,7 @@ meta_allowed(tags,	     list(string)).
 meta_allowed(description,    string).
 meta_allowed(commit_message, string).
 
-%%	storage_get(+Request, +Format) is det.
+%%	storage_get(+Request, +Format, +Options) is det.
 %
 %	HTTP handler that returns information a given gitty file.
 %
@@ -238,9 +243,9 @@ meta_allowed(commit_message, string).
 %	     Reply with diff relative to RelTo.  Default is the
 %	     previous commit.
 
-storage_get(Request, swish) :-
-	swish_reply_config(Request), !.
-storage_get(Request, Format) :-
+storage_get(Request, swish, Options) :-
+	swish_reply_config(Request, Options), !.
+storage_get(Request, Format, _) :-
 	setting(directory, Dir),
 	request_file_or_hash(Request, Dir, FileOrHash, Type),
 	storage_get(Format, Dir, Type, FileOrHash, Request).
diff --git a/lib/swish/swish_debug.pl b/lib/swish/swish_debug.pl
index d4039c4..1a2aaea 100644
--- a/lib/swish/swish_debug.pl
+++ b/lib/swish/swish_debug.pl
@@ -28,7 +28,8 @@
 */
 
 :- module(swish_debug,
-	  [ pengine_stale_module/1,	% -Module
+	  [ pengine_stale_module/1,	% -Module, -State
+	    pengine_stale_module/2,	% -Module, -State
 	    swish_statistics/1,		% -Statistics
 	    start_swish_stat_collector/0,
 	    swish_stats/2		% ?Period, ?Dicts
@@ -38,13 +39,18 @@
 :- use_module(library(lists)).
 :- use_module(library(apply)).
 :- use_module(library(debug)).
+:- use_module(library(aggregate)).
 :- use_module(procps).
 :- use_module(highlight).
+:- if(exists_source(library(mallocinfo))).
+:- use_module(library(mallocinfo)).
+:- endif.
 
-%%	pengine_stale_module(-M) is nondet.
+%%	pengine_stale_module(-M, -State) is nondet.
 %
 %	True if M seems to  be  a   pengine  module  with  no associated
-%	pengine.
+%	pengine. State is a dict that describes   what we know about the
+%	module.
 
 pengine_stale_module(M) :-
 	current_module(M),
@@ -52,11 +58,38 @@ pengine_stale_module(M) :-
 	\+ live_module(M),
 	\+ current_highlight_state(M, _).
 
+pengine_stale_module(M, State) :-
+	pengine_stale_module(M),
+	stale_module_state(M, State).
+
 live_module(M) :-
 	pengine_property(Pengine, module(M)),
 	pengine_property(Pengine, thread(Thread)),
 	catch(thread_property(Thread, status(running)), _, fail).
 
+stale_module_state(M, State) :-
+	findall(N-V, stale_module_property(M, N, V), Properties),
+	dict_create(State, stale, Properties).
+
+stale_module_property(M, pengine, Pengine) :-
+	pengine_property(Pengine, module(M)).
+stale_module_property(M, pengine_queue, Queue) :-
+	pengine_property(Pengine, module(M)),
+	pengines:pengine_queue(Pengine, Queue, _TimeOut, _Time).
+stale_module_property(M, pengine_pending_queue, Queue) :-
+	pengine_property(Pengine, module(M)),
+	pengines:output_queue(Pengine, Queue, _Time).
+stale_module_property(M, thread, Thread) :-
+	pengine_property(Pengine, module(M)),
+	pengine_property(Pengine, thread(Thread)).
+stale_module_property(M, thread_status, Status) :-
+	pengine_property(Pengine, module(M)),
+	pengine_property(Pengine, thread(Thread)),
+	catch(thread_property(Thread, status(Status)), _, fail).
+stale_module_property(M, program_space, Space) :-
+	module_property(M, program_space(Space)).
+
+
 %%	swish_statistics(?State)
 %
 %	True if State is a statistics about SWISH
@@ -64,7 +97,9 @@ live_module(M) :-
 swish_statistics(highlight_states(Count)) :-
 	aggregate_all(count, current_highlight_state(_,_), Count).
 swish_statistics(pengines(Count)) :-
-	aggregate_all(count, pengine_property(_,self(_)), Count).
+	aggregate_all(count, pengine_property(_,thread(_)), Count).
+swish_statistics(remote_pengines(Count)) :-
+	aggregate_all(count, pengine_property(_,remote(_)), Count).
 swish_statistics(pengines_created(Count)) :-
 	(   flag(pengines_created, Old, Old)
 	->  Count = Old
@@ -100,8 +135,13 @@ uuid_code(_, X) :- char_type(X, xdigit(_)).
 		 *	     STATISTICS		*
 		 *******************************/
 
+:- if(current_predicate(http_unix_daemon:http_daemon/0)).
+:- use_module(library(broadcast)).
+:- listen(http(post_server_start), start_swish_stat_collector).
+:- else.
 :- initialization
 	start_swish_stat_collector.
+:- endif.
 
 %%	start_swish_stat_collector
 %
@@ -142,7 +182,7 @@ swish_stat_collector(Thread, Dims, Interval) :-
 %	  Number of running pengines
 %	  * pengines_created
 %	  Total number of pengines created
-%	  * d_pengines
+%	  * d_pengines_created
 %	  Pengines created per second
 %	  * rss
 %	  Total resident memory
@@ -210,20 +250,32 @@ reply_stats_request(Client-get_stats(Period), SlidingStat) :-
 
 swish_stats(stats{ cpu:CPU,
 		   rss:RSS,
+		   fordblks:Fordblks,
 		   stack:Stack,
 		   pengines:Pengines,
-		   pengines_created:PenginesCreated
+		   pengines_created:PenginesCreated,
+		   time:Time
 		 }) :-
+	get_time(Now),
+	Time is floor(Now),
 	statistics(process_cputime, PCPU),
 	statistics(cputime, MyCPU),
 	CPU is PCPU-MyCPU,
 	statistics(stack, Stack),
+	fordblks(Fordblks),
 	catch(procps_stat(Stat), _,
 	      Stat = stat{rss:0}),
 	RSS = Stat.rss,
 	swish_statistics(pengines(Pengines)),
 	swish_statistics(pengines_created(PenginesCreated)).
 
+:- if(current_predicate(mallinfo/1)).
+fordblks(Fordblks) :-
+	mallinfo(MallInfo),
+	Fordblks = MallInfo.fordblks.
+:- endif.
+
+
 /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Maintain sliding statistics. The statistics are maintained in a ring. If
 the ring wraps around, the average is pushed to the next ring.
@@ -305,7 +357,6 @@ avg_key(Dicts, Len, Key, Key-Avg) :-
 	sandbox:safe_primitive/1.
 
 sandbox:safe_primitive(swish_debug:pengine_stale_module(_)).
+sandbox:safe_primitive(swish_debug:pengine_stale_module(_,_)).
 sandbox:safe_primitive(swish_debug:swish_statistics(_)).
 sandbox:safe_primitive(swish_debug:swish_stats(_, _)).
-
-
diff --git a/lib/swish/trace.pl b/lib/swish/trace.pl
index 3772053..f009aad 100644
--- a/lib/swish/trace.pl
+++ b/lib/swish/trace.pl
@@ -28,7 +28,7 @@
 */
 
 :- module(swish_trace,
-	  [ '$swish wrapper'/1		% +Goal
+	  [ '$swish wrapper'/2		% +Goal, -Residuals
 	  ]).
 :- use_module(library(debug)).
 :- use_module(library(settings)).
@@ -54,7 +54,7 @@
 :- set_prolog_flag(generate_debug_info, false).
 
 :- meta_predicate
-	'$swish wrapper'(0).
+	'$swish wrapper'(0, -).
 
 /** <module>
 
@@ -177,7 +177,7 @@ strip_stack(error(Error, context(prolog_stack(S), Msg)),
 	nonvar(S).
 strip_stack(Error, Error).
 
-%%	'$swish wrapper'(:Goal)
+%%	'$swish wrapper'(:Goal, -Residuals)
 %
 %	Wrap a SWISH goal in '$swish  wrapper'. This has two advantages:
 %	we can detect that the tracer is   operating  on a SWISH goal by
@@ -186,9 +186,11 @@ strip_stack(Error, Error).
 
 :- meta_predicate swish_call(0).
 
-'$swish wrapper'(Goal) :-
+'$swish wrapper'(Goal, '$residuals'(Residuals)) :-
 	catch(swish_call(Goal), E, throw(E)),
 	deterministic(Det),
+	Goal = M:_,
+	residuals(M, Residuals),
 	(   tracing,
 	    Det == false
 	->  (   notrace,
@@ -210,6 +212,30 @@ no_lco.
 :- '$hide'(no_lco/0).
 
 
+%%	residuals(+PengineModule, -Goals:list(callable)) is det.
+%
+%	Find residual goals  that  are  not   bound  to  the  projection
+%	variables. We must do so while  we   are  in  the Pengine as the
+%	goals typically live in global variables   that  are not visible
+%	when formulating the answer  from   the  projection variables as
+%	done in library(pengines_io).
+%
+%	This relies on the SWI-Prolog 7.3.14 residual goal extension.
+
+:- if(current_predicate(prolog:residual_goals//0)).
+residuals(TypeIn, Goals) :-
+	phrase(prolog:residual_goals, Goals0),
+	maplist(unqualify_residual(TypeIn), Goals0, Goals).
+
+unqualify_residual(M, M:G, G) :- !.
+unqualify_residual(T, M:G, G) :-
+	predicate_property(T:G, imported_from(M)), !.
+unqualify_residual(_, G, G).
+:- else.
+residuals(_, []).
+:- endif.
+
+
 		 /*******************************
 		 *	  SOURCE LOCATION	*
 		 *******************************/
@@ -558,6 +584,7 @@ prolog_clause:open_source(File, Stream) :-
 
 exception_hook(Ex, Ex, _Frame, Catcher) :-
 	Catcher \== none,
+	Catcher \== 'C',
 	prolog_frame_attribute(Catcher, predicate_indicator, PI),
 	debug(trace(exception), 'Ex: ~p, catcher: ~p', [Ex, PI]),
 	PI == '$swish wrapper'/1,
@@ -596,6 +623,7 @@ sandbox:safe_primitive(system:notrace).
 sandbox:safe_primitive(system:tracing).
 sandbox:safe_primitive(edinburgh:debug).
 sandbox:safe_primitive(system:deterministic(_)).
+sandbox:safe_primitive(swish_trace:residuals(_,_)).
 
 
 		 /*******************************
diff --git a/web/help/help.html b/web/help/help.html
index ecee09e..a47ac56 100644
--- a/web/help/help.html
+++ b/web/help/help.html
@@ -5,6 +5,13 @@
   <title>SWISH: SWI-Prolog for SHaring</title>
   </head>
 <body>
+<style>
+p.note { margin-left: 5%; position: relative }
+p.note span.glyphicon-hand-right {
+float: left; font-size: 150%; color: orange; padding-right: 0.2em;
+}
+</style>
+
 <h4>Table of Contents</h4>
   <ul>
     <li><a href="#help-basics">Basic operation</a></li>
@@ -26,9 +33,10 @@ against the <i>built-in</i> predicates of Prolog.  For example:
 ?- format("Hello world!~n").</pre>
 
 <p>
-A query can be executed by hitting <code>RETURN</code> if the query
-is complete and the caret is behind the '.' that terminates the query
-or by using the <b>Run!</b> button.  At this moment, the following happens:
+A query can be executed by hitting <code>RETURN</code> if the query is
+<em>complete</em> (i.e., ends in a full-stop) or by using the <a
+class="btn btn-xs btn-primary">Run!</a> button. At this moment, the
+following happens:
 
 <ol>
   <li>The interface creates a <em>runner</em> in the top-right window
@@ -48,11 +56,16 @@ or by using the <b>Run!</b> button.  At this moment, the following happens:
 </ol>
 
 <p>
-Note that <b>you do not have to save your program to execute it</b>.  If
-your are not satisfied with the answer to a query, you can simply
-edit the program and use the <b>Run!</b> again.  The new query is
-executed in a completely new environment.  In particular, data that
-you asserted in a previous query is not available in the next.
+Note that <b>you do not have to save your program to execute it</b>. If
+your are not satisfied with the answer to a query, you can simply edit
+the program and use <a class="btn btn-xs btn-primary">Run!</a> again.
+The new query is executed in a completely new environment. In
+particular, data that you asserted in a previous query is not available
+in the next.
+
+<p class="note">
+<span class="glyphicon glyphicon-hand-right"></span> Use
+<strong>Ctrl-Enter</strong> to insert a newline in a complete query
 
 <h2 id="help-examples">Embedding examples in the program text</h2>
 <p>
@@ -119,13 +132,17 @@ that provides shared editing, chat and voice communication.
 <p>
 After running a query, the <strong>complete</strong> result set for the
 query can be downloaded as a CSV (Comma Separated Values) document by
-clicking the double down arrow at the <em>runner</em> window. This
-causes a dialogue to appear that allows for specifying the columns,
-optionally the detailed result format (if the server provides multiple
-result formats), whether only <em>distinct</em> results should be
-returned and the maximum number of results to return. The latter is by
-default set to 10&nbsp;000 to avoid sending huge documents by accident.
-The field can be cleared to return all results.
+clicking the <span class="glyphicon glyphicon-download"></span> button
+at the top-right of a <em>runner</em> window or using the
+<strong>Download answers as CSV</strong> option from the <span
+class="glyphicon glyphicon-menu-hamburger"></span> button on
+<em>notebook query cells</em>. This causes a dialogue to appear that
+allows for specifying the columns, optionally the detailed result format
+(if the server provides multiple result formats), whether only
+<em>distinct</em> results should be returned and the maximum number of
+results to return. The latter is by default set to 10&nbsp;000 to avoid
+sending huge documents by accident. The field can be cleared to return
+all results.
 
 <h3>Download query results through an API</h3>
 <p>
@@ -198,6 +215,7 @@ providing the parameters below.  The URL accepts both `GET` and `POST` requests.
 
 <pre>
 http://swish.swi-prolog.org/?code=https://github.com/SWI-Prolog/swipl-devel/raw/master/demo/likes.pl&amp;q=likes(sam,Food).</pre>
+<a target="_blank" href="http://swish.swi-prolog.org/?code=https://github.com/SWI-Prolog/swipl-devel/raw/master/demo/likes.pl&amp;q=likes(sam,Food).">Try it!</a> (launches a new tab)
 
 </body>
 </html>
diff --git a/web/help/notebook.html b/web/help/notebook.html
index 1c70cb5..ef786d8 100644
--- a/web/help/notebook.html
+++ b/web/help/notebook.html
@@ -2,10 +2,15 @@
 
 <html>
   <head>
+  <title>About Prolog notebooks</title>
   </head>
 <body>
-<h2>About Prolog notebooks</h2>
-
+<style>
+p.note { margin-left: 5%; margin-top: 1em; position: relative }
+p.note span.glyphicon-hand-right {
+float: left; font-size: 120%; color: orange; padding-right: 0.2em;
+}
+</style>
 <p>
 A notebook is a list of <i>cells</i>. Notebooks are different from a
 program with examples because they can <em>tell a story</em>, mixing
@@ -40,14 +45,24 @@ and moving cells.
 <li>
 To <b>link to another notebook</b>, create a markdown cell and use the
 markdown notation <code>[label](reference)</code>, e.g.,
-
-<pre style="font-size:80%">
-[All about lists](lists.swinb)
-</pre>
+<code>[All about lists](lists.swinb)</code>
 <li>
 To <b>link to file in the store</b>, create a markdown cell and use the
 markdown notation <code>[label](myfile.pl)</code>, e.g.,
+<code>[Basic predicates](basics.pl)</code>
 </ul>
 
+<h3>Notebook queries and programs</h3>
+
+A notebook <em>query</em> cell is executed against the program cell
+<strong>above it</strong> and all program cells marked as
+<em>background</em> using the <a class="btn btn-success btn-xs"><span
+style="color:#bef" class="glyphicon glyphicon-cloud"></span></a> button.
+
+<p class="note"><span class="glyphicon glyphicon-hand-right"></span>
+Longer fragments of code that are required throughout a notebook and
+possibly on multiple notebooks are defined in a <em>program</em> tab,
+saved and included using, e.g., <code>:- include(mybasics).</code> in a
+background program kept at the bottom of the notebook.
 </body>
 </html>
diff --git a/web/help/runner.html b/web/help/runner.html
index 3afe1dd..752053c 100644
--- a/web/help/runner.html
+++ b/web/help/runner.html
@@ -85,9 +85,10 @@ queries.
 
 <h2>Get results as CSV or through an API</h2>
 <p>
-Variable bindings can be downloaded as CSV using the double down arrow
-at the top-right of a runner or using an appropriate client library to
-access the web API. See the <b>Help/help ...</b> for details.
+Variable bindings can be downloaded as CSV using the <span
+class="glyphicon glyphicon-download"></span> button at the top-right of
+a runner or using an appropriate client library to access the web API.
+See the <b>Help/help ...</b> for details.
 
 </body>
 </html>
diff --git a/web/icons/wip.png b/web/icons/wip.png
new file mode 100644
index 0000000..62dbd0e
Binary files /dev/null and b/web/icons/wip.png differ