jam/js/dos/scheduler.js

916 lines
28 KiB
JavaScript

/**
** ==============================
** O O O OOOO
** O O O O O O
** O O O O O O
** OOOO OOOO O OOO OOOO
** O O O O O O O
** O O O O O O O
** OOOO OOOO O O OOOO
** ==============================
** Dr. Stefan Bosse http://www.bsslab.de
**
** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED
** BY THE AUTHOR(S).
** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED,
** MODIFIED, OR OTHERWISE USED IN A CONTEXT
** OUTSIDE OF THE SOFTWARE SYSTEM.
**
** $AUTHORS: Stefan Bosse
** $INITIAL: (C) 2006-2016 bLAB
** $CREATED: 24-3-15 by sbosse.
** $VERSION: 1.3.1
**
** $INFO:
**
** DOS: Task Scheduler
**
** $ENDOFINFO
*/
"use strict";
var log = 0;
var timestamp=1;
var Io = Require('com/io');
var util = Require('util');
var Comp = Require('com/compat');
var Json = Require('jam/jsonfn');
var String = Comp.string;
var Array = Comp.array;
var Perv = Comp.pervasives;
var trace = Io.tracing;
/**
*
* @type {undefined|taskcontext}
*/
var current = undefined;
/**
*
* @type {undefined|taskscheduler}
*/
var current_scheduler = undefined;
var scheduling = false;
var TICK=10;
/*
** Program time with TICK resolution
*/
var time=0;
/**
*
* @type {undefined|object}
*/
var timer=undefined;
/** The process context class
*
* @param {string} id
* @param {* []} [trans]
* @param {object} [obj]
* @constructor
* @typedef {{id,blocked,trans,block,obj,timeout,timer,state}} taskcontext~obj
* @see taskcontext~obj
*/
var taskcontext = function (id,trans,obj) {
this.id=id;
this.blocked=false;
this.event=0;
this.trans=trans;
this.block=[];
this.obj=obj;
this.timeout=0;
this.timer=0;
this.state=undefined;
};
/** The scheduler class
*
* @constructor
* @typedef {{context,callbacks,handler,current:taskcontext,nextid,lock,nested:number,reschedule:number}} taskscheduler~obj
* @see taskscheduler~obj
*/
var taskscheduler = function () {
var self=this;
this.context=[new taskcontext('root',[],self)];
this.callbacks=[];
this.handler=[];
this.current=undefined;
this.nextid=0;
this.lock=0;
this.nested=0;
this.reschedule=0;
current_scheduler=self;
};
/**
** Add a callback function executed once in the next scheduler run
** before any process (context) activity execution.
*
*
* @param {function|[]} callback that is fun or [fun] or [fun,arg1,arg2,..,arg9]
*/
taskscheduler.prototype.add_callback = function(callback) {
this.callbacks.push(callback);
};
/**
** Add a timer handler function executed once or cont. by the scheduler.
*
*
* @param {number} timeout
* @param {string} name
* @param {function} callback
* @param {boolean} once
*/
taskscheduler.prototype.add_timer = function(timeout,name,callback,once) {
var self=this;
var cont = new taskcontext(name);
Object.preventExtensions(cont);
var trans=[
[
undefined,
function () {
if (once==true) {
cont.timeout = 0;
cont.timer=0;
cont.blocked = true;
} else {
cont.timer = time + cont.timeout;
cont.blocked = true;
}
callback(cont)
},
function () {return !cont.blocked; }
]
];
cont.trans=trans;
cont.timeout=timeout;
cont.timer=time+timeout;
cont.blocked=true;
this.handler.push(cont);
};
/**
* Register a child scheduler function called on events (e.g., schedule next).
*/
taskscheduler.prototype.link = function(callback) {
this.schedulers.push(callback);
};
taskscheduler.prototype.log = function (v) {log=v};
/**
** Remove a timer handler identified by its name.
*
*
* @param {string} name
*/
taskscheduler.prototype.remove_timer = function(name) {
var i;
loop: for(i in this.handler) {
var handler=this.handler[i];
if (String.equal(handler.id,name))
{
this.handler.splice(i,1);
break loop;
}
}
};
/*
** next:
** fun
** [fun]
** [fun,arg1,arg2,..,arg9]
*/
function exec_block_fun(next) {
var fun = next[0]||next,
argn = next.length-1;
switch (argn) {
case 0:
case -1:
fun(); break;
case 1: fun(next[1]); break;
case 2: fun(next[1],next[2]); break;
case 3: fun(next[1],next[2],next[3]); break;
case 4: fun(next[1],next[2],next[3],next[4]); break;
case 5: fun(next[1],next[2],next[3],next[4],next[5]); break;
case 6: fun(next[1],next[2],next[3],next[4],next[5],next[6]); break;
case 7: fun(next[1],next[2],next[3],next[4],next[5],next[6],next[7]); break;
case 8: fun(next[1],next[2],next[3],next[4],next[5],next[6],next[7],next[8]); break;
case 9: fun(next[1],next[2],next[3],next[4],next[5],next[6],next[7],next[8],next[9]); break;
default:
Io.err('Schedule.exec_block_fun: more than 9 function arguments');
}
}
function schedule_block(con,manage) {
var next;
/*
** Process current function block sequence first!
** Format: [[fun,arg1,arg2,...],[block2], [block3], ..]
** Simplified: [fun,fun,...]
*/
if (!con.blocked) {
next = con.block[0];
con.block.splice(0,1);
/*
** Do no execute handler blocks maybe at the end of a subsection
** of the block list.
*/
while (!Array.empty(con.block) && next.handler!=undefined) {
next = con.block[0];
con.block.splice(0,1);
}
if (next.handler==undefined) {
current = con;
manage.scheduled++;
manage.scheduledcon++;
Io.log((log<2)||('Schedule [B], starting context '+current.id+' block['+con.block.length+']'));
Io.trace(trace||('Schedule [B], starting context '+current.id));
try {exec_block_fun(next)} catch(e) {
/*
** Iterate through the block list and try to find a handler entry.
*/
while (next.handler==undefined && !Array.empty(con.block)) {
next = con.block[0];
con.block.splice(0,1);
}
if (next.handler!=undefined) {
/*
** Call handler ...
*/
Io.log((log<2)||('[SCHE] executing exception handler for error '+e));
// console.log(next.handler.toString())
try {exec_block_fun([next.handler,e])}
catch (e) {
Io.out('Schedule [B], in context '+con.id+', got exception in exception handler: '+e);
// Io.printstack(e);
Io.out(Json.stringify(next).replace(/\\n/g,'\n'));
};
} else {
Io.out('Schedule [B], in context '+con.id+', got uncaught exception in schedule block: '+e);
// Io.printstack(e);
Io.out(Json.stringify(next).replace(/\\n/g,'\n'));
}
}
Io.log((log<2)||('Schedule [B], end context '+current.id+' blocked='+con.blocked+' block['+con.block.length+']'));
Io.trace(trace||('Schedule [B], end context '+current.id+' blocked='+con.blocked+' block['+con.block.length+']'));
if (con.blocked) manage.blocked++;
}
}
}
/** Add a new process context
**
*
* @param {taskcontext} c
*/
taskscheduler.prototype.Add = function(c) {
this.context.push(c);
};
taskscheduler.prototype.Delay = function(millisec) {
current.timer=time+millisec;
current.timeout=0;
current.blocked=true;
};
taskscheduler.prototype.GetCurrent = function() {
return current;
};
taskscheduler.prototype.SetCurrent = function(context) {
var _current=current; current=context;
return _current;
};
taskscheduler.prototype.TaskContext = function(id,proc) {
var obj = new taskcontext(id,proc.transitions(),proc);
Object.preventExtensions(obj);
return obj;
};
/** Init scheduler
**
*
*/
taskscheduler.prototype.Init = function() {
var self=this;
var i,con;
time=0;
current_scheduler=self;
/*
** Set the root context
*/
current=self.context[0];
setInterval(function () {
// TBD lock
time=time+TICK;
for (i in self.context) {
con = self.context[i];
if (con.timer > 0 && con.timer <= time) {
// Timeout event occurred
con.timer=0;
con.blocked=false;
}
}
for (i in self.handler) {
con = self.handler[i];
if (con.timer > 0 && con.timer <= time && Array.empty(con.block)) {
// Timeout event occurred
// Schedule only handler with empty scheduling block!
Io.log((log<20)||('TIMEOUT '+con.timeout+' '+time));
con.timer=0;
con.blocked=false;
}
}
// TBD unlock
},TICK)
};
/** Scheduler run loop using timeout calls
*
*/
taskscheduler.prototype.Run = function() {
var i,con,
self=this,
reschedule=self.reschedule,
nexttime=0;
scheduling=true;
Io.trace(trace||('Run('+this.nested+')..'));
this.nested++;
if (timer != undefined) clearTimeout(timer);
while (self.Schedule()>0 || reschedule>0) {
reschedule=self.reschedule;
self.reschedule=Perv.max(0,self.reschedule-1);
}
// if there are only blocked processes start a timer calling the scheduler later...
for (i in self.context) {
con = self.context[i];
if (con.timeout>0) nexttime=(nexttime==0?con.timer:Perv.min(nexttime,con.timer));
}
for (i in self.handler) {
con = self.handler[i];
if (con.timeout>0) nexttime=(nexttime==0?con.timer:Perv.min(nexttime,con.timer));
}
/**
* If there are no timeout processes, we need no scheduling.
* But a timeout is required for proper JS processing. Each event
* will restart the timer!
*/
if (nexttime==0) nexttime=time+TICK*10;
timer=setTimeout(function () {self.Run ()},nexttime-time);
this.nested--;
Io.trace(trace||('Run('+this.nested+')+ '+(nexttime-time)));
scheduling=false;
};
/** One schedule iteration
*
* @returns {number}
*/
taskscheduler.prototype.Schedule = function() {
var i, j,con,cond,trans,state,state0,state1,remove,
manage={
scheduled:0,
blocked:0,
scheduledcon:0};
//var log=2;
// TBD lock
Io.trace(trace||('Schedule Start'));
Io.log((log<2)||('[SCHE '+Perv.mtime()+'] ('+time+') Schedule'));
/*
** First the urgent callbacks, if any, A callback may not suspend the execution!
** (Pure computational statements)
*/
for (i in this.callbacks) {
var callback=this.callbacks[i];
Io.log((log<2)||('[SCHE] executing callback ['+Array.length(callback)+']'));
exec_block_fun(callback);
}
this.callbacks=[];
/*
** Then the handlers, if any..
*/
var handler=[];
for (i in this.handler) {
con=this.handler[i];
remove=false;
Io.log((log<10)||('handler '+util.inspect(con)));
if (con.block && !Array.empty(con.block)) {
// what to do? timer handler is always blocked!
Io.log((log<2)||('Schedule [H], checking context '+con.id));
schedule_block(con,manage);
if (Array.empty(con.block)) {
/*
** Restore timer handler
*/
if (con.timeout<=0) {
// must be removed
remove=true;
} else {
con.blocked=true;
manage.blocked++;
con.state = state0;
if (con.timeout>0) con.timer = time + con.timeout;
}
}
}
else {
trans:for(j in con.trans) {
trans=con.trans[j];
state=con.state;
/*
** Transition row: [current activity function, next acitivity function, optional condition function]
*/
state0=trans[0];
state1=trans[1];
cond=trans[2];
if (!con.blocked && state==state0 && (cond==undefined || cond(cond.obj))) {
con.state=state1;
current=con;
manage.scheduled++;
Io.log((log<2)||('Schedule [H], starting context '+current.id));
Io.trace(trace||('Schedule [H], starting context '+current.id));
state1();
if (con.block && !Array.empty(con.block)) {
// a new block was added in the timer handler, unblock this handler!
con.blocked=false;
remove=false;
// must be restored after the block list was executed!
}
else {
if (con.timeout<=0) {
// must be removed
remove=true;
} else {
if (con.blocked) manage.blocked++;
con.state = state0;
}
}
}
}
}
if (!remove) handler.push(con);
}
this.handler=handler;
/*
** Finally the context processes ..
*/
for (i in this.context) {
manage.scheduledcon=0;
con=this.context[i];
/*
** First the sequential statement blocks of the context process..
*/
if (con.block && !Array.empty(con.block)) {
schedule_block(con,manage);
}
if (!con.blocked && manage.scheduledcon==0) {
/*
** Second the transitional section of the context process, if any (maybe empty)
*/
trans:for (j in con.trans) {
trans = con.trans[j];
state = con.state;
state0 = trans[0];
state1 = trans[1];
cond = trans[2];
if (!con.blocked && state == state0 && (cond==undefined || cond(con.obj))) {
con.state = state1;
current = con;
Io.log((log<2)||('Schedule [T], starting context '+current.id));
Io.trace(trace||('Schedule [T], starting context '+current.id));
manage.scheduled++;
state1.call(con.obj);
if (con.blocked) manage.blocked++;
break trans;
}
}
};
}
if (current.blocked) {
/*
** Try to find a non-blocked context (root?)
*/
loop: for (i in this.context) {
con = this.context[i];
if (!con.blocked) {
current = con;
break loop;
}
}
}
// TBD unlock
Io.log((log<2)||('[SCHE '+Perv.mtime()+'] ('+time+') End '+(manage.scheduled+manage.scheduledcon)));
Io.trace(trace||('Schedule End('+(manage.scheduled+manage.scheduledcon)+')'));
return (manage.scheduled+manage.scheduledcon);
};
/**
*
* Mutex Lock Object
* May only be used in scheduling blocks (last statement of a block element)!!
*
* @constructor
* @typedef {{locked:bool,waiter:[],owner:taskcontext}} lock~obj
* @see lock~obj
*/
var lock = function() {
this.locked = false;
this.waiter = [];
this.owner = undefined;
};
lock.prototype.acquire = function () {
if (!this.locked) {
this.locked = true;
this.owner = current;
} else {
this.waiter.push(current);
current.blocked=true;
}
};
lock.prototype.try_acquire = function () {
if (!this.locked) {
this.locked = true;
return true;
} else {
return false;
}
};
lock.prototype.release = function () {
if (!Array.empty(this.waiter)) {
var next = Array.head(this.waiter);
this.waiter = Array.tail(this.waiter);
next.blocked = false;
} else {
this.locked = false;
this.owner=undefined;
}
};
lock.prototype.init = function () {
this.locked = false;
this.owner = undefined;
this.waiter = [];
};
lock.prototype.is_locked = function () {
return this.locked;
};
var modu = {
TICK:TICK,
time:time,
/**
*
* @param timeout
* @param name
* @param {function([context])} callback
* @param [once]
*/
AddTimer: function(timeout,name,callback,once) {
if (current_scheduler) current_scheduler.add_timer(timeout,name,callback,once);
this.ScheduleNext();
},
/**
*
* @param name
*/
RemoveTimer: function(name) {
if (current_scheduler) current_scheduler.remove_timer(name);
},
/**
*
* @returns {taskscheduler}
*/
TaskScheduler: function() {
var obj = new taskscheduler();
Object.preventExtensions(obj);
return obj;
},
/** Create a new transitional task context (virtual process)
* The process object consists of activity functions and a transition function.
* The object transition function variable must have the name 'transitions'.
* The transition function must return an array in the format:
* [
* [undefined,act_start] // initial start activity
* [act1,act2,function(self){return <cond>}], // conditional transitions
* [act2,act3] // unconditional transition
* ..
* ]
* A transition from an outgoing activity function (of the process) act_i
* to another acitivty function a_j occurs only: 1. If the context is not blocked;
* 2. If the condition is satisfied.
*
* @param {string} id
* @param {object} proc
* @returns {taskcontext}
*/
TaskContext: function(id,proc) {
var obj = new taskcontext(id,proc.transitions(),proc);
Object.preventExtensions(obj);
return obj;
},
/** Create and initialize a new transitional task context from a process constructor.
* Simplified version of TaskContext. Returns the created process object.
*
* Proc = function (arg) {
* this.a1 = function () {}; ..
* this.a2 = function () {}; ..
* this.transitions = [ [ai,aj,cond], .. ];
* this.on = { err: function () {}, ..}; *optional*
* }
*
*
*/
NewTask: function (id,Proc,arg) {
var proc, context;
proc = new Proc(arg);
context = new taskcontext(id,proc.transitions,proc);
proc.context=context;
current_scheduler.Add(proc.context);
return proc;
},
/** Create and add a new functional task context. Inside the function
* scheduling blocks and loops can be used.
*
* @param {taskscheduler|undefined} sched
* @param id
* @param fun
* @param [arg]
* @returns {taskcontext}
*/
FunContext: function(sched,id,fun,arg) {
var Sch=this, proccon, proc;
if (sched==undefined) sched=current_scheduler;
proccon = function () {
var self=this;
this.act =function(){fun(arg)};
this.transitions = function(){
var trans;
trans = [
[undefined,this.act]
];
return trans;
};
this.context = Sch.TaskContext(id, self);
};
proc = new proccon();
sched.Add(proc.context);
return proc.context;
},
Bind: function (object, method) {
return method.bind(object);
},
Delay: function(millisec) {
current.timer=time+millisec;
current.timeout=0;
current.blocked=true;
},
GetId: function() {return current.id;},
/** Return current context
*
* @returns {undefined|taskcontext}
*/
GetCurrent: function() {return current;},
/** Return current context
*
* @param [taskcontext]
* @returns {undefined|taskcontext}
*/
SetCurrent: function(context) {var _current=current; current=context; return _current},
/** Get current scheduler
*
* @returns {undefined|taskscheduler}
*/
GetScheduler: function() {return current_scheduler;},
/** Get current system time
*
* @returns {number}
*/
GetTime: function() {return time;},
/** Call the scheduler ASAP.
* Usefull after a Wakeup call.
*/
Schedule: function() {
Io.trace(trace||('Schedule'));
if (!scheduling && current_scheduler != undefined) {
if (timer != undefined) clearTimeout(timer);
timer=undefined;
//timer=setTimeout(function () {current_scheduler.Run ()},0);
current_scheduler.Run ()
} else {
current_scheduler.reschedule++;
}
},
/** Call the scheduler ASAP, eventually with a callback function executed first.
* Must be preemption save!
*
*
* @param [callback]
* @param [args]
*/
ScheduleNext: function(callback,args) {
Io.trace(trace||('ScheduleNext'));
if (!scheduling && current_scheduler != undefined) {
if (callback) current_scheduler.add_callback(callback,args);
if (timer != undefined) clearTimeout(timer);
timer=undefined;
//timer=setTimeout(function () {current_scheduler.Run ()},0);
current_scheduler.Run ()
} else {
if (callback) current_scheduler.add_callback(callback,args);
current_scheduler.reschedule++;
}
},
/**
** Schedule an asynchronous callback function execution.
* Must be preemption-save! If we are currently scheduling, queue the
* callback, otherwise execute it immediately,
*
*
* @param {function|[]} callback that is fun or [fun] or [fun,arg1,arg2,..,arg9]
*
*/
ScheduleCallback: function(callback) {
Io.trace(trace||('ScheduleNext'));
if (!scheduling) {
scheduling=true;
exec_block_fun(callback);
scheduling=false;
/*
** Pending scheduler run?
*/
if (current_scheduler.reschedule>0) {
if (timer != undefined) clearTimeout(timer);
timer=undefined;
//timer=setTimeout(function () {current_scheduler.Run ()},0);
current_scheduler.Run ()
}
} else {
current_scheduler.add_callback(callback);
current_scheduler.reschedule++;
}
},
/**
** Add a scheduler function execution block to the current context.
** Each entry in the block array (a partition) is executed in the given order sequentially.
** Each entry of [[fun,arg1,arg2,...],[block2], [block3], ..] may block (suspend execution).
** Notes:
** - Bind methods to respective objects: [Sch.Bind(ob,obj.method),..]
** - The scheduler block must be the last (and(or only) statement in an activity or a function.
* - A blocking statement must be the last statement in a block partition.
** - The current context activity may NOT be blocked with a Sch.Suspend operation!!
*
*
* @param {* []} block [[fun,arg1,arg2,...],[block2], [block3], ..] or simplified [fun,fun,..]
* @param {function} [handler] optional exception handler function fun(exception)
*/
ScheduleBlock: function(block,handler) {
/*
** Schedule sequence of functions that may block (suepend execution of current context process).
** If there are already block elements, add the new
** block elements to the top (!!) of the current block.
*/
if (handler!=undefined ) block.push({handler:handler});
if (current.block.length == 0) current.block=block;
else current.block=Array.merge(block,current.block);
},
/**
*
* @param {function(index:number):boolean} cond function() { return cond; }
* @param {* []} body [[fun,arg1,arg2,...],[block2], [block3], ..] or simplified [fun,fun,..]
* @param {* []} [finalize] optional loop finalize block
* @param {function} [handler] optional exception handler function fun(exception)
*
* Note: NEVER raise an exception in a callback called in a block function if there is a handler here!
* Another still active handler can be executed instead!
*/
ScheduleLoop: function(cond,body,finalize,handler) {
var self=this;
var index=0;
/*
** Iterate and schedule a block
* cond: function(index) { return cond; }
*/
var block = [
function() {
if (cond(index)) {
self.ScheduleBlock(body.slice());
} else if (finalize!=undefined) {
self.ScheduleBlock(finalize);
}
},
function() {
index++;
},
function () {
if (cond(index)) {
self.ScheduleBlock(block.slice());
}
else if (finalize!=undefined) {
self.ScheduleBlock(finalize);
}
}
];
if (handler!=undefined ) block.push({handler:handler});
self.ScheduleBlock(block.slice());
},
/**
*
* @param blocked
* @param [context]
*/
SetBlocked: function(blocked,context) {
if (context==undefined) {
current.blocked=blocked;
} else {
context.blocked=blocked;
if (context.blocked==false && context.parent) {
if (context.parent.blocked) {
context.parent.blocked=false;
}
// console.log(context.parent.id);
context.parent.event++;
}
}
},
/**
*
* @param context
* @returns {*|boolean}
*/
IsBlocked: function(context) {
if (context==undefined)
return current.blocked;
else
return context.blocked;
},
/**
*
* @param context
*/
Suspend: function(context) {
Io.log((log<3)||('[SCH] Suspend: '+(context?context.id:current.id) ));
if (context==undefined)
current.blocked=true;
else
context.blocked=true;
return current;
},
/** Wake up a context. Modifies only the context.
* Use Schedule() to force ASAP scheduling.
*
* @param context
*/
Wakeup: function(context) {
context.blocked = false;
if (context.parent) {
if (context.parent.blocked) {
context.parent.blocked=false;
}
// console.log(context.parent);
context.parent.event++;
}
Io.log((log<3)||('[SCH] Wakekup: '+context.id));
},
/**
** Mutex Lock Object
** May only be used in scheduling blocks (last statement of a block element)!!
*
* @returns {lock}
*/
Lock: function() {
var obj = new lock();
Object.preventExtensions(obj);
return obj;
}
};
module.exports=modu;
modu.B = module.exports.ScheduleBlock.bind(module.exports);
modu.L = module.exports.ScheduleLoop.bind(module.exports);
global.B = module.exports.ScheduleBlock.bind(module.exports);
global.L = module.exports.ScheduleLoop.bind(module.exports);
global.Delay = module.exports.Delay.bind(module.exports);