Mercurial > hg > dml-open-cliopatria
diff cpack/dml/lib/async.pl @ 0:718306e29690 tip
commiting public release
author | Daniel Wolff |
---|---|
date | Tue, 09 Feb 2016 21:05:06 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cpack/dml/lib/async.pl Tue Feb 09 21:05:06 2016 +0100 @@ -0,0 +1,469 @@ +/* Part of DML (Digital Music Laboratory) + Copyright 2014-2015 Samer Abdallah, University of London + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +:- module(async, + [ async_memo/4 + , async_current_job/5 + , async_thread_progress/4 + , async_get_progress_callback/1 + , async_set_progress_callback/1 + , async_unset_progress_callback/0 + , async_cancel/3 + , async_cancel/2 + , with_progress_stack/1 + , with_cont/4 + , simple_task/2 + , stepwise_task/4 + , updatable_status_task/3 + , map_with_progress/3 + , map_fold_with_progress/5 + , with_global/3 + , with_new_ref/3 + ]). + +/** <module> Asynchronous memoised computations with progress and partial results + + This module provides services for managing memoised computations in a thread pool + with the ability to check on the progress of running jobs and receive partial + results if they are available. Completed computations are memoised using + library(memo). + + ---++ Job control + The first half of the module is concerned with managing memoised computations. + Computations are requested using async_memo/4. If the result is already available in + the memoised database, the result is returned. If the computation is already running, + status information about the job is returned. Otherwise, a new job is started. Status + information is returned as the type =|async_status(A)|=. + == + async_status(A) ---> done(metadata) + ; initiated(job_id,natural) + ; waiting(natural) + ; running(job_id,A)) + ; recompute(job_id,natural,metadata). + + job_id == atom. + == + The type =|metadata|= is defined in library(memo). + + + ---++ Helper meta-predicates + The second half of the module provides a collection of predicates for computations + to use to handle requests for progress information, in the form of a stack of tasks. + It recognises the following levels of progress information: + == + summary :: progress_level(list(any)). + partial_result :: progress_level(any). + == +*/ +:- meta_predicate + async_set_progress_callback(2) + , async_memo(+,0,-,+) + , with_global(+,+,0) + , with_new_ref(-,+,0) + , with_progress_stack(0) + , with_progress_handler(2,0) + , with_cont(+,1,2,-) + , simple_task(+,0) + , stepwise_task(+,+,-,0) + % , stepwise_partial_task(+,+,-,-,0) + % , fold_with_progress(3,+,+,-) + , map_with_progress(2,+,-) + , map_fold_with_progress(2,3,+,+,-). + +:- use_module(library(thread_pool)). +:- use_module(library(insist)). +:- use_module(library(memo)). +:- use_module(library(dcg_core),[seqmap/4]). + +%% async_memo(+P:thread_pool, +G:memo_goal, -S:async_status(_), Opts:options) is det. +% +% Start running a goal asynchronously in a thread pool if it is not +% already running. +% In order for the goal to provide progress information, it must use nb_setval/2 +% to set the global variable =|progress|=. +% +% Options: +% * status_var(Var:status_var(A)) +% If computation is already running, information of type =|A|= +% associated with job is returned in Stats. Default is =|progress(summary)|=. +% * recompute(R:oneof([none,failed,test(Pred)])) +% If =|none|=, then a previously failed computation is not retried, and the +% status =|done(Comp-Result)|= is returned, where Comp includes +% the computation time and host and Result is =|fail|= or =|ex(Exception)|=. +% If =|failed|=, unsuccessful computations are retried and the status +% =|recomputing(ID,Pos,Meta)|= is returned, where =|ID|= is the ID of the +% new job, =|Pos|= its initial position in the queue, and =|Meta=Comp-Result|= +% the status of the previous computation. +% If =|test(Pred)|=, then the computation is retried if =|call(Pred,Meta)|= +% succeeds, where Meta is the memoisied computation metadata and the variables +% in Goal will be unified with their values resulting from that computation. +% The status returned is =|initiated(ID,Pos)|=. Pred must include its module +% specifier as a I am too lazy to worry about meta-predicate options right now. +% * globals(list(pair(atom,any))) +% Set global variables to given values in new thread. +% +% The value of the =|status_var(_)|= option determines the information returned +% if the status of the computation is =|running(ID,StatusVals)|=. +% == +% progress(progress_level(A)) :: status_var(A). +% stats(list(stats_key)) :: status_var(list(stats_vals)). +% time :: status_var(pair(time,float)). % start and elapsed times +% status_var(A)-status_var(B) :: status_var(pair(A,B)). +% +% time==float. % as returned by get_time/1. +% == +% The type =|stats_key|= denotes any atom accepted by thread_statistics/3, and +% =|stats_vals|= the type of values returned. + +async_memo(_,Goal,done(Meta),Opts) :- + option(recompute(none),Opts,none), + browse(Goal,Meta), !. + +async_memo(Pool,Goal,Status,Opts) :- + with_mutex(async,unsafe_async_memo(Pool,Goal,Status,Opts)). + +unsafe_async_memo(Pool,Goal,Status,Opts) :- + option(globals(Globals),Opts,[]), + ( browse(Goal,Meta) + -> option(recompute(Recompute),Opts,none), + ( recompute(Recompute,Meta) + -> clear_all(Goal,Meta), + spawn(Pool,Goal,Globals,ID,Waiting), + Status=recomputing(ID,Waiting,Meta) + ; Status=done(Meta) + ) + ; % !!! it's possible that another thread might be computing + % this goal and complete at this point. If this happens, + % that job will not be detected and a new a job will be started + % here. If that happens, the newly created job will find the result + % and complete quickly. + ( async_current_job(Pool,Goal,ID,T0,S) + -> option(progress_levels(Levels),Opts,[summary]), + % !!! it's possible that the job will complete here + % and so the progress request will create an error + goal_status(ID,T0,Levels,S,Status) + ; spawn(Pool,Goal,Globals,ID,Waiting), + Status=spawned(ID,Waiting) + ) + ). + +recompute(failed,_-fail). +recompute(failed,_-ex(_)). +recompute(test(P),M) :- call(P,M). + +goal_status(ID, TSubmit, _, waiting(Pos), waiting(ID,TSubmit,Pos)). +goal_status(ID, _, Levels, running(Thread,TStart), running(ID,TStart,Thread,Progress)) :- + catch( ( async_thread_progress(Thread,Levels,T,Vals), Progress=just(T-Vals)), Ex, + ( print_message(warning,error_getting_job_progress(Thread,Ex)), Progress=nothing)). + +spawn(Pool,Goal,Globals,ID,Waiting) :- + uuid(ID), + (thread_pool_property(Pool,backlog(Waiting)) -> true; Waiting=0), + get_time(Time), + setup_call_catcher_cleanup( + recordz(Pool,job(ID,Time,Goal),Ref), + thread_create_in_pool(Pool,spawnee(ID,Goal,Globals),_, + [ detached(true), at_exit(erase(Ref))]), + exception(_), + erase(Ref)). + +spawnee(ID,Goal,Globals) :- + setup_call_cleanup( + with_mutex(async, + ( recorded(ID,async:cancelled,Ref) + -> debug(async,'Cancelled before starting: ~q',[Goal]), + erase(Ref), fail + ; pairs_keys_values(Globals,GNames,GVals), + maplist(nb_setval,GNames,GVals), + get_time(T0), + thread_self(Thread), + recordz(ID,running(Thread,T0),Ref) + )), + ( freeze(Res,check_result(Res)), % prevent memoisation if abort(_) is caught + debug(async,"Running asyc goal ~q",[Goal]), + memo(Goal,_-Res)), + erase(Ref)). + +check_result(Ex) :- + ( Ex=ex(abort(Reason)) + -> debug(async,'Computation aborted (~w)',[Reason]), + throw(abort(Reason)) + ; true + ). + +%% async_current_job(+Pool:thread_pool, -Goal:callable, -ID:job_id, -T:time, -S:job_status) is nondet. +% Retrieves information about current pending jobs. A job is pending if it is running +% or waiting to run in the specified thread pool. T is the time the job was submitted. +% Job status is: +% == +% job_status ---> waiting(natural) % queued at given postion +% ; running(thread_id,time). % with start time +% == +async_current_job(Pool,Goal,ID,Time,Status) :- + recorded(Pool,job(ID,Time,Goal)), % this finds ALL jobs + ( recorded(ID,running(Thread,StartTime)) + -> Status=running(Thread,StartTime) + ; thread_pool_property(Pool,waiting(Waiting)), + nth1(Pos,Waiting,create(Pool,async:spawnee(ID,_,_),_,_,_)) + -> Status=waiting(Pos) + ; print_message(warning, async_job_status_indeterminate(Pool,ID,Goal)), + Status=indeterminate % could be finished, or transferring from waiting to running + ). + + +%% async_thread_progress(+Thread:thread_id,+Levels:list(progress_level), -T:time, -Vals:list) is det. +% Called in main thread to get information about job running in thread Thread. +% Will throw an exception if: +% - the thread has terminated +% - the thread does not respond with progress info in less than 3 seconds +% - the thread does not have a progress handler call back registered +% - the progress handler fails on any of the progress levels requested. +async_thread_progress(Thread,Levels,Time,Vals) :- + thread_self(Me), + gensym(prog,RID), % progress request ID + flush_all_messages(Me,progress(_,_,_,_)), + thread_signal(Thread,report_progress(RID,Levels,Me)), + ( thread_get_message(Me,progress(RID,Time,Vals,St),[timeout(3)]) -> true + ; throw(get_thread_progress_timeout(Thread,Levels)) + ), + debug(async,'Got progress ~w from thread ~w: ~w',[Levels,Thread,Vals]), + memo:reflect(St). + +flush_all_messages(Queue,Pattern) :- + repeat, \+thread_get_message(Queue,Pattern,[timeout(0)]), !. + +%% report_progress(+RID:atomic, +Var:status_var(A), +Thread:thread_id) is det. +% Called from job thread in response to a signal, to send progress information to thread Thread. +report_progress(RID,Levels,Thread) :- + get_time(Time), + memo:reify(async:my_progress(Levels,Vals),St), !, + debug(async,'reporting progress(~w): ~q',[Levels,Vals]), + thread_send_message(Thread,progress(RID,Time,Vals,St)). + +my_progress(Levels,Vals) :- nb_getval(async_progress,G), insist(maplist(G,Levels,Vals)). + + +%% async_cancel(+Pool:thread_pool, +ID:job_id, +Ex:term) is det. +%% async_cancel(+Pool:thread_pool, +ID:job_id) is det. +% Cancel job with given ID, if it is currently pending. If it is running, the +% exception term Ex is thrown (=|abort(cancel)|= in async_cancel/2). +% If not, the recorded database is used to mark the job for immediate failure +% as soon as it is started. +async_cancel(Pool,ID) :- async_cancel(Pool,ID,abort(cancel)). +async_cancel(Pool,ID,Ex) :- + debug(async,'Cancelling ~w:~w with ~w',[Pool,ID,Ex]), + with_mutex(async, (async_current_job(Pool,_,ID,_,S) -> cancel(ID,S,Ex); true)). + +cancel(ID,waiting(_),_) :- recordz(ID,async:cancelled). +cancel(_, running(Thread,_),Ex) :- + catch( thread_signal(Thread,throw(Ex)), E, + print_message(warning,error_signalling_job_thread(Thread,E))). +cancel(ID,indeterminate,_) :- print_message(warning,indeterminate_job_state_on_cancel(ID)). + +%% async_set_progress_callback(+P:pred(+Level:any,-Info:any)) is det. +% Sets the progress handler for the current thread. +async_set_progress_callback(Pred) :- nb_setval(async_progress,Pred). + +%% async_get_progress_callback(-P:pred(+Level:any,-Info:any)) is semidet. +% Gets the progress handler for the current thread, if one has been set. +async_get_progress_callback(Pred) :- nb_current(async_progress,Pred). + +%% async_unset_progress_callback is det. +% Unsets the progress handler for the current thread. Subsequent calls to +% async_get_progress_callback/1 will fail. +async_unset_progress_callback :- nb_delete(async_progress). + +% ----------------------------------------------------------------------- + + +%% with_progress_stack(+Goal:callable) is nondet. +% +% Runs Goal with the current thread's progress handler (which must be initially +% unset) set to an empty task stack. Tasks (goals which run in cooperation with a +% a progress handler) can be pushed on to the task stack using with_progress_handler/2. +% Fails if progress callback has already been set to something else. +% +% The progress handler recognises progress level terms: +% * summary :: progress_level(list(any)) +% Yields a list of progress terms, one for each entry in the task stack. +% * partial_result :: progress_level(any) +% Yields partial results of the computation if they are available. +with_progress_stack(Goal) :- + async_get_progress_callback(CB), !, + CB=async:get_stack_progress, + call(Goal). +with_progress_stack(Goal) :- + with_global(async_progress_stack, nil, + setup_call_cleanup( + async_set_progress_callback(get_stack_progress), Goal, + async_unset_progress_callback)). + +%% get_stack_progress(+Level:progress_level(A),-Info:A) is semidet. +% Gets the request level of progress information from the currently installed +% progress stack. +get_stack_progress(summary,P) :- + nb_getval(async_progress_stack,H), + call(H,progress(summary),P). + +get_stack_progress(partial_result,R) :- + nb_getval(async_progress_stack,H), + call(H,partial_result,R). + + +%% with_progress_handler(+H:progress_handler, +Goal:callable) is nondet. +% Pushes the progress handler H to the top of the task stack associated with +% the current thread and calls Goal. The handler is removed on exit. +with_progress_handler(H,Goal) :- + nb_getval(async_progress_stack,H0), call(H0,push(H),H1), + setup_call_cleanup( nb_setval(async_progress_stack,H1), Goal, + nb_setval(async_progress_stack,H0)). + +nil(progress(_),[]). +nil(push(H),chain(H,nil)). +nil(partial_result,nothing). +nil(partial_result(Cont),R) :- call(Cont,nothing,R). + +chain(Head,Tail1,push(H),chain(Head,Tail2)) :- call(Tail1,push(H),Tail2). +chain(Head,Tail,progress(L),[PH|PT]) :- + call(Head,progress(L),PH), + call(Tail,progress(L),PT). +chain(Head,Tail,partial_result,MR) :- + call(Tail,partial_result,R1), + (call(Head,partial_result(R1),R) -> MR=just(R); MR=nothing). + + +%% simple_task(+Desc,+Goal:callable) is det. +% +% Calls goal with a progress handler that reports Desc when progress +% information is requested. No partial results are available. +simple_task(Desc,Goal) :- with_progress_handler(simple_progress(Desc),Goal). + +simple_progress(Desc,progress(_),Desc). + + +%% with_cont(+Desc,+Pred:pred(-A),+Cont:pred(+A,-B),-Result:B) is det. +% +% Calls Pred and then passes the result to Cont yielding Result. +% A progress handler is installed such that any partial results yielded by +% subtasks in Pred are passed to Cont to yield a partial result. Progress requests +% yield Desc. +with_cont(Desc,Pred,Cont,Result) :- + with_progress_handler(cont(Desc,Cont), + (call(Pred,R),call(Cont,R,Result))). + +cont(_,Cont,Tail1,push(H),cont(Cont,Tail2)) :- call(Tail1,push(H),Tail2). +cont(Desc,_,progress(_),Desc). +cont(_,Cont,partial_result(just(R1)),R) :- call(Cont,R1,R). + +%% stepwise_task(+Desc,+Total:nonneg,-Step:callable,+Goal:callable) is det. +% +% Calls Goal with a progress handler that is able to report progress as a term +% =|stepwise(Desc,Done/Total)|=. Goal is permitted to call Step in order to increment +% the =|Done|= counter. Partial results are not available. +stepwise_task(Desc,Total,async:increment(Counter),Goal) :- + with_new_ref(Counter,0, with_progress_handler(stepwise_progress(Desc,Total,Counter), Goal)). + +stepwise_progress(Desc,Total,Counter,progress(_),stepwise(Desc,Done/Total)) :- b_getval(Counter,Done). + + +%% updatable_status_task(+Initial,-Update:pred(any),+Goal:callable) is det. +% +% Calls goal with Update set to a predicate which can be used to set the +% current status of the task. This will be reported if progress is requested. +% No partial results are available. Initial is the status before Update is called. +:- meta_predicate updatable_status_task(+,-,0). +updatable_status_task(Initial,async:set_ref(Status),Goal) :- + with_new_ref(Status,Initial, with_progress_handler(status_progress(Status),Goal)). + +status_progress(Status,progress(_),S) :- b_getval(Status,S). + + +map_with_progress(P,Xs,Ys) :- + length(Xs,Total), + stepwise_task(map(P),Total,Step, maplist(call_and_step(Step,P),Xs,Ys)). + +call_and_step(Step,P,X,Y) :- call(P,X,Y), call(Step). + +/* +fold_with_progress(Folder,Items,S0,S1) :- + length(Items,N), + stepwise_partial_task(folding(Folder),N,Step,Update, + seqmap(step_update(Step,Update,Folder),Items,S0,S1)). + +step_update(Step,Update,P,X,S0,S1) :- + call(P,X,S0,S1), + call(Update,S1), + call(Step). +*/ + +:- setting(map_fold_batch,nonneg,512,"Batch size for flushing fold queue."). + +%% map_fold_with_progress(+Mapper:pred(A,B), +Folder:pred(list(B),S,S), +Items:list(A), +S1:S, -S2:S) is det. +% +% Map Mapper over Items and fold over the results with Folder, with progress information +% partial results available. Note that Folder operates on a _list_ of items, not just one +% item at a time. Mapper is used to accumulate up to N items before folding them batchwise +% into the accumulator, where N is the value of the setting =|map_fold_batch|=. +% +% Progress information is returned as a term =|stepwise(Desc,Done/Total)|=, where +% =|Desc=map_fold(Mapper,Folder)|=. If partial results are requested, the current batch +% of mapped items is first folded in before returning the value of the accumulator. +map_fold_with_progress(Mapper,Folder,Items,S1,S2) :- + length(Items,Total), + setting(map_fold_batch,M1), + with_new_ref(StateRef,s(Items,0,M1,Head,Head,S1), + ( Handler=map_fold_progress(map_fold(Mapper,Folder),Total,StateRef), + with_progress_handler(Handler, + ( map_fold_loop(Mapper,Folder,StateRef), + call(Handler,partial_result(nothing),S2))))). + +map_fold_loop(Mapper,Folder,StateRef) :- + b_getval(StateRef,s(Xs1,N1,M1,Head,Ys1,S)), + ( Xs1=[] -> Ys1=[] + ; Xs1=[X|Xs2] + -> call(Mapper,X,Y), !, + Ys1=[Y|Ys2], succ(N1,N2), + State1=s(Xs2,N2,M2,Head,Ys2,S), + ( succ(M2,M1) -> State2=State1 + ; simple_task(folding,flush_fold(Folder,State1,State2)) + ), + b_setval(StateRef,State2), !, + map_fold_loop(Mapper,Folder,StateRef) + ). + +map_fold_progress(Desc,Total,StateRef,progress(_),stepwise(Desc,Done/Total)) :- + b_getval(StateRef,s(_,Done,_,_,_,_)). +map_fold_progress(map_fold(_,Folder),_,StateRef,partial_result(_),S2) :- + b_getval(StateRef,State1), + flush_fold(Folder,State1,State2), + b_setval(StateRef,State2), + State2=s(_,_,_,_,_,S2). + +flush_fold(Folder,s(Xs,Done,_,Head,[],S1),s(Xs,Done,M1,H2,H2,S2)) :- + setting(map_fold_batch,M1), + call(Folder,Head,S1,S2), !. + +% for managing global variable +new_ref(Ref) :- gensym('$ref',Ref). +new_ref(Ref,Val) :- new_ref(Ref), b_setval(Ref,Val). + +with_global(Name,Val,Goal) :- setup_call_cleanup(b_setval(Name,Val), Goal, nb_delete(Name)). +with_new_ref(Name,Val,Goal) :- new_ref(Name), with_global(Name,Val,Goal). + +set_ref(Ref,Val) :- nb_setval(Ref,Val). +increment(Counter) :- b_getval(Counter,N), succ(N,M), b_setval(Counter,M).