https://github.com/akkartik/mu/blob/master/073scheduler.cc

      1 //: Run a second routine concurrently using 'start-running', without any
      2 //: guarantees on how the operations in each are interleaved with each other.
      3
      4 :(scenario scheduler)
      5 def f1 [
      6   start-running f2
      7   # wait for f2 to run
      8   {
      9     jump-unless 1:num, -1
     10   }
     11 ]
     12 def f2 [
     13   1:num <- copy 1
     14 ]
     15 +schedule: f1
     16 +schedule: f2
     17
     18 //: first, add a deadline to run(routine)
     19 :(before "End Globals")
     20 int Scheduling_interval = 500;
     21 :(before "End routine Fields")
     22 int instructions_run_this_scheduling_slice;
     23 :(before "End routine Constructor")
     24 instructions_run_this_scheduling_slice = 0;
     25 :(after "Running One Instruction")
     26  ++Current_routine->instructions_run_this_scheduling_slice;
     27 :(replace{} "bool should_continue_running(const routine* current_routine)")
     28 bool should_continue_running(const routine* current_routine) {
     29   assert(current_routine == Current_routine);  // argument passed in just to make caller readable above
     30   return Current_routine->state == RUNNING
     31       && Current_routine->instructions_run_this_scheduling_slice < Scheduling_interval;
     32 }
     33 :(after "stop_running_current_routine:")
     34 // Reset instructions_run_this_scheduling_slice
     35 Current_routine->instructions_run_this_scheduling_slice = 0;
     36
     37 //: now the rest of the scheduler is clean
     38
     39 :(before "struct routine")
     40 enum routine_state {
     41   RUNNING,
     42   COMPLETED,
     43   // End routine States
     44 };
     45 :(before "End routine Fields")
     46 enum routine_state state;
     47 :(before "End routine Constructor")
     48 state = RUNNING;
     49
     50 :(before "End Globals")
     51 vector<routine*> Routines;
     52 int Current_routine_index = 0;
     53 :(before "End Reset")
     54 Scheduling_interval = 500;
     55 for (int i = 0;  i < SIZE(Routines);  ++i)
     56   delete Routines.at(i);
     57 Routines.clear();
     58 Current_routine = NULL;
     59 :(replace{} "void run(const recipe_ordinal r)")
     60 void run(const recipe_ordinal r) {
     61   run(new routine(r));
     62 }
     63
     64 :(code)
     65 void run(routine* rr) {
     66   Routines.push_back(rr);
     67   Current_routine_index = 0, Current_routine = Routines.at(0);
     68   while (!all_routines_done()) {
     69     skip_to_next_routine();
     70     assert(Current_routine);
     71     assert(Current_routine->state == RUNNING);
     72     trace(9990, "schedule") << current_routine_label() << end();
     73     run_current_routine();
     74     // Scheduler State Transitions
     75     if (Current_routine->completed())
     76       Current_routine->state = COMPLETED;
     77     // End Scheduler State Transitions
     78
     79     // Scheduler Cleanup
     80     // End Scheduler Cleanup
     81   }
     82   // End Run Routine
     83 }
     84
     85 bool all_routines_done() {
     86   for (int i = 0;  i < SIZE(Routines);  ++i) {
     87     if (Routines.at(i)->state == RUNNING)
     88       return false;
     89   }
     90   return true;
     91 }
     92
     93 // skip Current_routine_index past non-RUNNING routines
     94 void skip_to_next_routine() {
     95   assert(!Routines.empty());
     96   assert(Current_routine_index < SIZE(Routines));
     97   for (int i = (Current_routine_index+1)%SIZE(Routines);  i != Current_routine_index;  i = (i+1)%SIZE(Routines)) {
     98     if (Routines.at(i)->state == RUNNING) {
     99       Current_routine_index = i;
    100       Current_routine = Routines.at(i);
    101       return;
    102     }
    103   }
    104 }
    105
    106 string current_routine_label() {
    107   return routine_label(Current_routine);
    108 }
    109
    110 string routine_label(routine* r) {
    111   ostringstream result;
    112   const call_stack& calls = r->calls;
    113   for (call_stack::const_iterator p = calls.begin();  p != calls.end();  ++p) {
    114     if (p != calls.begin()) result << '/';
    115     result << get(Recipe, p->running_recipe).name;
    116   }
    117   return result.str();
    118 }
    119
    120 //: special case for the very first routine
    121 :(replace{} "void run_main(int argc, char* argv[])")
    122 void run_main(int argc, char* argv[]) {
    123   recipe_ordinal r = get(Recipe_ordinal, "main");
    124   assert(r);
    125   routine* main_routine = new routine(r);
    126   // pass in commandline args as ingredients to main
    127   // todo: test this
    128   Current_routine = main_routine;
    129   for (int i = 1;  i < argc;  ++i) {
    130     vector<double> arg;
    131     arg.push_back(new_mu_text(argv[i]));
    132     assert(get(Memory, arg.back()) == 0);
    133     current_call().ingredient_atoms.push_back(arg);
    134   }
    135   run(main_routine);
    136 }
    137
    138 //:: To schedule new routines to run, call 'start-running'.
    139
    140 //: 'start-running' will return a unique id for the routine that was created.
    141 //: routine id is a number, but don't do any arithmetic on it
    142 :(before "End routine Fields")
    143 int id;
    144 :(before "End Globals")
    145 int Next_routine_id = 1;
    146 :(before "End Reset")
    147 Next_routine_id = 1;
    148 :(before "End routine Constructor")
    149 id = Next_routine_id;
    150 ++Next_routine_id;
    151
    152 //: routines save the routine that spawned them
    153 :(before "End routine Fields")
    154 // todo: really should be routine_id, but that's less efficient.
    155 int parent_index;  // only < 0 if there's no parent_index
    156 :(before "End routine Constructor")
    157 parent_index = -1;
    158
    159 :(before "End Primitive Recipe Declarations")
    160 START_RUNNING,
    161 :(before "End Primitive Recipe Numbers")
    162 put(Recipe_ordinal, "start-running", START_RUNNING);
    163 :(before "End Primitive Recipe Checks")
    164 case START_RUNNING: {
    165   if (inst.ingredients.empty()) {
    166     raise << maybe(get(Recipe, r).name) << "'start-running' requires at least one ingredient: the recipe to start running\n" << end();
    167     break;
    168   }
    169   if (!is_mu_recipe(inst.ingredients.at(0))) {
    170     raise << maybe(get(Recipe, r).name) << "first ingredient of 'start-running' should be a recipe, but got '" << to_string(inst.ingredients.at(0)) << "'\n" << end();
    171     break;
    172   }
    173   break;
    174 }
    175 :(before "End Primitive Recipe Implementations")
    176 case START_RUNNING: {
    177   routine* new_routine = new routine(ingredients.at(0).at(0));
    178   new_routine->parent_index = Current_routine_index;
    179   // populate ingredients
    180   for (int i = /*skip callee*/1;  i < SIZE(current_instruction().ingredients);  ++i) {
    181     new_routine->calls.front().ingredient_atoms.push_back(ingredients.at(i));
    182     reagent/*copy*/ ingredient = current_instruction().ingredients.at(i);
    183     new_routine->calls.front().ingredients.push_back(ingredient);
    184     // End Populate start-running Ingredient
    185   }
    186   Routines.push_back(new_routine);
    187   products.resize(1);
    188   products.at(0).push_back(new_routine->id);
    189   break;
    190 }
    191
    192 :(scenario scheduler_runs_single_routine)
    193 % Scheduling_interval = 1;
    194 def f1 [
    195   1:num <- copy 0
    196   2:num <- copy 0
    197 ]
    198 +schedule: f1
    199 +run: {1: "number"} <- copy {0: "literal"}
    200 +schedule: f1
    201 +run: {2: "number"} <- copy {0: "literal"}
    202
    203 :(scenario scheduler_interleaves_routines)
    204 % Scheduling_interval = 1;
    205 def f1 [
    206   start-running f2
    207   1:num <- copy 0
    208   2:num <- copy 0
    209 ]
    210 def f2 [
    211   3:num <- copy 0
    212   4:num <- copy 0
    213 ]
    214 +schedule: f1
    215 +run: start-running {f2: "recipe-literal"}
    216 +schedule: f2
    217 +run: {3: "number"} <- copy {0: "literal"}
    218 +schedule: f1
    219 +run: {1: "number"} <- copy {0: "literal"}
    220 +schedule: f2
    221 +run: {4: "number"} <- copy {0: "literal"}
    222 +schedule: f1
    223 +run: {2: "number"} <- copy {0: "literal"}
    224
    225 :(scenario start_running_takes_ingredients)
    226 def f1 [
    227   start-running f2, 3
    228   # wait for f2 to run
    229   {
    230     jump-unless 1:num, -1
    231   }
    232 ]
    233 def f2 [
    234   1:num <- next-ingredient
    235   2:num <- add 1:num, 1
    236 ]
    237 +mem: storing 4 in location 2
    238
    239 //: type-checking for 'start-running'
    240
    241 :(scenario start_running_checks_types)
    242 % Hide_errors = true;
    243 def f1 [
    244   start-running f2, 3
    245 ]
    246 def f2 n:&:num [
    247 ]
    248 +error: f1: ingredient 0 has the wrong type at 'start-running f2, 3'
    249
    250 // 'start-running' only uses the ingredients of the callee, not its products
    251 :(before "End is_indirect_call_with_ingredients Special-cases")
    252 if (r == START_RUNNING) return true;
    253
    254 //: back to testing 'start-running'
    255
    256 :(scenario start_running_returns_routine_id)
    257 def f1 [
    258   1:num <- start-running f2
    259 ]
    260 def f2 [
    261   12:num <- copy 44
    262 ]
    263 +mem: storing 2 in location 1
    264
    265 //: this scenario will require some careful setup in escaped C++
    266 //: (straining our tangle capabilities to near-breaking point)
    267 :(scenario scheduler_skips_completed_routines)
    268 % recipe_ordinal f1 = load("recipe f1 [\n1:num <- copy 0\n]\n").front();
    269 % recipe_ordinal f2 = load("recipe f2 [\n2:num <- copy 0\n]\n").front();
    270 % Routines.push_back(new routine(f1));  // f1 meant to run
    271 % Routines.push_back(new routine(f2));
    272 % Routines.back()->state = COMPLETED;  // f2 not meant to run
    273 # must have at least one routine without escaping
    274 def f3 [
    275   3:num <- copy 0
    276 ]
    277 # by interleaving '+' lines with '-' lines, we allow f1 and f3 to run in any order
    278 +schedule: f1
    279 +mem: storing 0 in location 1
    280 -schedule: f2
    281 -mem: storing 0 in location 2
    282 +schedule: f3
    283 +mem: storing 0 in location 3
    284
    285 :(scenario scheduler_starts_at_middle_of_routines)
    286 % Routines.push_back(new routine(COPY));
    287 % Routines.back()->state = COMPLETED;
    288 def f1 [
    289   1:num <- copy 0
    290   2:num <- copy 0
    291 ]
    292 +schedule: f1
    293 -run: idle
    294
    295 //:: Errors in a routine cause it to terminate.
    296
    297 :(scenario scheduler_terminates_routines_after_errors)
    298 % Hide_errors = true;
    299 % Scheduling_interval = 2;
    300 def f1 [
    301   start-running f2
    302   1:num <- copy 0
    303   2:num <- copy 0
    304 ]
    305 def f2 [
    306   # divide by 0 twice
    307   3:num <- divide-with-remainder 4, 0
    308   4:num <- divide-with-remainder 4, 0
    309 ]
    310 # f2 should stop after first divide by 0
    311 +error: f2: divide by zero in '3:num <- divide-with-remainder 4, 0'
    312 -error: f2: divide by zero in '4:num <- divide-with-remainder 4, 0'
    313
    314 :(after "operator<<(ostream& os, end /*unused*/)")
    315   if (Trace_stream && Trace_stream->curr_label == "error" && Current_routine) {
    316     Current_routine->state = COMPLETED;
    317   }
    318
    319 //:: Routines are marked completed when their parent completes.
    320
    321 :(scenario scheduler_kills_orphans)
    322 def main [
    323   start-running f1
    324   # f1 never actually runs because its parent completes without waiting for it
    325 ]
    326 def f1 [
    327   1:num <- copy 0
    328 ]
    329 -schedule: f1
    330
    331 :(before "End Scheduler Cleanup")
    332 for (int i = 0;  i < SIZE(Routines);  ++i) {
    333   if (Routines.at(i)->state == COMPLETED) continue;
    334   if (Routines.at(i)->parent_index < 0) continue;  // root thread
    335   // structured concurrency: http://250bpm.com/blog:71
    336   if (has_completed_parent(i)) {
    337     Routines.at(i)->state = COMPLETED;
    338   }
    339 }
    340
    341 :(code)
    342 bool has_completed_parent(int routine_index) {
    343   for (int j = routine_index;  j >= 0;  j = Routines.at(j)->parent_index) {
    344     if (Routines.at(j)->state == COMPLETED)
    345       return true;
    346   }
    347   return false;
    348 }
    349
    350 //:: 'routine-state' can tell if a given routine id is running
    351
    352 :(scenario routine_state_test)
    353 % Scheduling_interval = 2;
    354 def f1 [
    355   1:num/child-id <- start-running f2
    356   12:num <- copy 0  # race condition since we don't care about location 12
    357   # thanks to Scheduling_interval, f2's one instruction runs in between here and completes
    358   2:num/state <- routine-state 1:num/child-id
    359 ]
    360 def f2 [
    361   12:num <- copy 0
    362   # trying to run a second instruction marks routine as completed
    363 ]
    364 # recipe f2 should be in state COMPLETED
    365 +mem: storing 1 in location 2
    366
    367 :(before "End Primitive Recipe Declarations")
    368 ROUTINE_STATE,
    369 :(before "End Primitive Recipe Numbers")
    370 put(Recipe_ordinal, "routine-state", ROUTINE_STATE);
    371 :(before "End Primitive Recipe Checks")
    372 case ROUTINE_STATE: {
    373   if (SIZE(inst.ingredients) != 1) {
    374     raise << maybe(get(Recipe, r).name) << "'routine-state' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
    375     break;
    376   }
    377   if (!is_mu_number(inst.ingredients.at(0))) {
    378     raise << maybe(get(Recipe, r).name) << "first ingredient of 'routine-state' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
    379     break;
    380   }
    381   break;
    382 }
    383 :(before "End Primitive Recipe Implementations")
    384 case ROUTINE_STATE: {
    385   int id = ingredients.at(0).at(0);
    386   int result = -1;
    387   for (int i = 0;  i < SIZE(Routines);  ++i) {
    388     if (Routines.at(i)->id == id) {
    389       result = Routines.at(i)->state;
    390       break;
    391     }
    392   }
    393   products.resize(1);
    394   products.at(0).push_back(result);
    395   break;
    396 }
    397
    398 //:: miscellaneous helpers
    399
    400 :(before "End Primitive Recipe Declarations")
    401 STOP,
    402 :(before "End Primitive Recipe Numbers")
    403 put(Recipe_ordinal, "stop", STOP);
    404 :(before "End Primitive Recipe Checks")
    405 case STOP: {
    406   if (SIZE(inst.ingredients) != 1) {
    407     raise << maybe(get(Recipe, r).name) << "'stop' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
    408     break;
    409   }
    410   if (!is_mu_number(inst.ingredients.at(0))) {
    411     raise << maybe(get(Recipe, r).name) << "first ingredient of 'stop' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
    412     break;
    413   }
    414   break;
    415 }
    416 :(before "End Primitive Recipe Implementations")
    417 case STOP: {
    418   int id = ingredients.at(0).at(0);
    419   for (int i = 0;  i < SIZE(Routines);  ++i) {
    420     if (Routines.at(i)->id == id) {
    421       Routines.at(i)->state = COMPLETED;
    422       break;
    423     }
    424   }
    425   break;
    426 }
    427
    428 :(before "End Primitive Recipe Declarations")
    429 _DUMP_ROUTINES,
    430 :(before "End Primitive Recipe Numbers")
    431 put(Recipe_ordinal, "$dump-routines", _DUMP_ROUTINES);
    432 :(before "End Primitive Recipe Checks")
    433 case _DUMP_ROUTINES: {
    434   break;
    435 }
    436 :(before "End Primitive Recipe Implementations")
    437 case _DUMP_ROUTINES: {
    438   for (int i = 0;  i < SIZE(Routines);  ++i) {
    439     cerr << i << ": " << Routines.at(i)->id << ' ' << Routines.at(i)->state << ' ' << Routines.at(i)->parent_index << '\n';
    440   }
    441   break;
    442 }
    443
    444 //: support for stopping routines after some number of cycles
    445
    446 :(scenario routine_discontinues_past_limit)
    447 % Scheduling_interval = 2;
    448 def f1 [
    449   1:num/child-id <- start-running f2
    450   limit-time 1:num/child-id, 10
    451   # padding loop just to make sure f2 has time to completed
    452   2:num <- copy 20
    453   2:num <- subtract 2:num, 1
    454   jump-if 2:num, -2:offset
    455 ]
    456 def f2 [
    457   jump -1:offset  # run forever
    458   $print [should never get here], 10/newline
    459 ]
    460 # f2 terminates
    461 +schedule: discontinuing routine 2
    462
    463 :(before "End routine States")
    464 DISCONTINUED,
    465 :(before "End Scheduler State Transitions")
    466 if (Current_routine->limit >= 0) {
    467   if (Current_routine->limit <= Scheduling_interval) {
    468     trace("schedule") << "discontinuing routine " << Current_routine->id << end();
    469     Current_routine->state = DISCONTINUED;
    470     Current_routine->limit = 0;
    471   }
    472   else {
    473     Current_routine->limit -= Scheduling_interval;
    474   }
    475 }
    476
    477 :(before "End Test Teardown")
    478 if (Passed && any_routines_with_error())
    479   raise << "some routines died with errors\n" << end();
    480 :(before "End Mu Test Teardown")
    481 if (Passed && any_routines_with_error())
    482   raise << Current_scenario->name << ": some routines died with errors\n" << end();
    483
    484 :(code)
    485 bool any_routines_with_error() {
    486   for (int i = 0;  i < SIZE(Routines);  ++i) {
    487     if (Routines.at(i)->state == DISCONTINUED)
    488       return true;
    489   }
    490   return false;
    491 }
    492
    493 :(before "End routine Fields")
    494 int limit;
    495 :(before "End routine Constructor")
    496 limit = -1;  /* no limit */
    497
    498 :(before "End Primitive Recipe Declarations")
    499 LIMIT_TIME,
    500 :(before "End Primitive Recipe Numbers")
    501 put(Recipe_ordinal, "limit-time", LIMIT_TIME);
    502 :(before "End Primitive Recipe Checks")
    503 case LIMIT_TIME: {
    504   if (SIZE(inst.ingredients) != 2) {
    505     raise << maybe(get(Recipe, r).name) << "'limit-time' requires exactly two ingredient, but got '" << to_original_string(inst) << "'\n" << end();
    506     break;
    507   }
    508   if (!is_mu_number(inst.ingredients.at(0))) {
    509     raise << maybe(get(Recipe, r).name) << "first ingredient of 'limit-time' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
    510     break;
    511   }
    512   if (!is_mu_number(inst.ingredients.at(1))) {
    513     raise << maybe(get(Recipe, r).name) << "second ingredient of 'limit-time' should be a number (of instructions to run for), but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
    514     break;
    515   }
    516   break;
    517 }
    518 :(before "End Primitive Recipe Implementations")
    519 case LIMIT_TIME: {
    520   int id = ingredients.at(0).at(0);
    521   for (int i = 0;  i < SIZE(Routines);  ++i) {
    522     if (Routines.at(i)->id == id) {
    523       Routines.at(i)->limit = ingredients.at(1).at(0);
    524       break;
    525     }
    526   }
    527   break;
    528 }
    529
    530 :(before "End routine Fields")
    531 int instructions_run;
    532 :(before "End routine Constructor")
    533 instructions_run = 0;
    534 :(before "Reset instructions_run_this_scheduling_slice")
    535 Current_routine->instructions_run += Current_routine->instructions_run_this_scheduling_slice;
    536 :(before "End Primitive Recipe Declarations")
    537 NUMBER_OF_INSTRUCTIONS,
    538 :(before "End Primitive Recipe Numbers")
    539 put(Recipe_ordinal, "number-of-instructions", NUMBER_OF_INSTRUCTIONS);
    540 :(before "End Primitive Recipe Checks")
    541 case NUMBER_OF_INSTRUCTIONS: {
    542   if (SIZE(inst.ingredients) != 1) {
    543     raise << maybe(get(Recipe, r).name) << "'number-of-instructions' requires exactly one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
    544     break;
    545   }
    546   if (!is_mu_number(inst.ingredients.at(0))) {
    547     raise << maybe(get(Recipe, r).name) << "first ingredient of 'number-of-instructions' should be a routine id generated by 'start-running', but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
    548     break;
    549   }
    550   break;
    551 }
    552 :(before "End Primitive Recipe Implementations")
    553 case NUMBER_OF_INSTRUCTIONS: {
    554   int id = ingredients.at(0).at(0);
    555   int result = -1;
    556   for (int i = 0;  i < SIZE(Routines);  ++i) {
    557     if (Routines.at(i)->id == id) {
    558       result = Routines.at(i)->instructions_run;
    559       break;
    560     }
    561   }
    562   products.resize(1);
    563   products.at(0).push_back(result);
    564   break;
    565 }
    566
    567 :(scenario number_of_instructions)
    568 def f1 [
    569   10:num/child-id <- start-running f2
    570   {
    571     loop-unless 20:num
    572   }
    573   11:num <- number-of-instructions 10:num
    574 ]
    575 def f2 [
    576   # 2 instructions worth of work
    577   1:num <- copy 34
    578   20:num <- copy 1
    579 ]
    580 # f2 runs an extra instruction for the implicit return added by the
    581 # fill_in_return_ingredients transform
    582 +mem: storing 3 in location 11
    583
    584 :(scenario number_of_instructions_across_multiple_scheduling_intervals)
    585 % Scheduling_interval = 1;
    586 def f1 [
    587   10:num/child-id <- start-running f2
    588   {
    589     loop-unless 20:num
    590   }
    591   11:num <- number-of-instructions 10:num
    592 ]
    593 def f2 [
    594   # 4 instructions worth of work
    595   1:num <- copy 34
    596   2:num <- copy 1
    597   2:num <- copy 3
    598   20:num <- copy 1
    599 ]
    600 # f2 runs an extra instruction for the implicit return added by the
    601 # fill_in_return_ingredients transform
    602 +mem: storing 5 in location 11
    603
    604 //:: make sure that each routine gets a different alloc to start
    605
    606 :(scenario new_concurrent)
    607 def f1 [
    608   start-running f2
    609   1:&:num/raw <- new number:type
    610   # wait for f2 to complete
    611   {
    612     loop-unless 4:num/raw
    613   }
    614 ]
    615 def f2 [
    616   2:&:num/raw <- new number:type
    617   # hack: assumes scheduler implementation
    618   3:bool/raw <- equal 1:&:num/raw, 2:&:num/raw
    619   # signal f2 complete
    620   4:num/raw <- copy 1
    621 ]
    622 +mem: storing 0 in location 3