{"action":"created","issue":{"url":"https://api.github.com/repos/nrwiersma/ESP8266Scheduler/issues/3","repository_url":"https://api.github.com/repos/nrwiersma/ESP8266Scheduler","labels_url":"https://api.github.com/repos/nrwiersma/ESP8266Scheduler/issues/3/labels{/name}","comments_url":"https://api.github.com/repos/nrwiersma/ESP8266Scheduler/issues/3/comments","events_url":"https://api.github.com/repos/nrwiersma/ESP8266Scheduler/issues/3/events","html_url":"https://github.com/nrwiersma/ESP8266Scheduler/issues/3","id":188968907,"number":3,"title":"Adding \"run_groups\" and \"data_spaces\"","user":{"login":"MikeFair","id":2304557,"avatar_url":"https://avatars.githubusercontent.com/u/2304557?v=3","gravatar_id":"","url":"https://api.github.com/users/MikeFair","html_url":"https://github.com/MikeFair","followers_url":"https://api.github.com/users/MikeFair/followers","following_url":"https://api.github.com/users/MikeFair/following{/other_user}","gists_url":"https://api.github.com/users/MikeFair/gists{/gist_id}","starred_url":"https://api.github.com/users/MikeFair/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MikeFair/subscriptions","organizations_url":"https://api.github.com/users/MikeFair/orgs","repos_url":"https://api.github.com/users/MikeFair/repos","events_url":"https://api.github.com/users/MikeFair/events{/privacy}","received_events_url":"https://api.github.com/users/MikeFair/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":5,"created_at":"2016-11-13T12:58:38Z","updated_at":"2016-11-13T21:05:26Z","closed_at":null,"body":"Hi!\r\n\r\nI've been working with the ESP8266Scheduler a bit and while I haven't used the official Arduino, or any other \"Scheduler\", libraries for comparison; I really like it!\r\n\r\nOne thing about the \"one-big-loop\" model is you have lots of control over the ordering of if/when things happen and they can share in-common data easily.  Scheduler largely eliminates that in favor of coroutines wrapped in their own classes that all run mostly segregated and seemingly in parallel.\r\n\r\nI've written some basic/initial code to add a \"run_groups\" concept to the Scheduler which adds back some degree of those coordination capabilities while keeping everything nice and separated. :)\r\n\r\nIn a Task code, either setup() or the constructor(), you'd add something like ```setRunGroupID(N);``` where currently N is a number 0..7 or 255 (the default).\r\n(The number of groups available is defined by the size of a bitfield used internally; I used an uint8_t.)\r\n\r\nBy assigning a Task a run_group_id, that Task will be delayed until all Tasks with lower run_group_ids have \"succeeded\" for this cycle.  \"Success\" means the Task considers its work completed and will wait for the next \"cycle\" before resuming.  So it could mean the Task \"timed out\" on what it was doing and didn't want to hold up downstream RunGroups from processing any longer.\r\n\r\nRunGroup 0xFF is the default RunGroup, and it's also the exception to the rule. Tasks with run_group_id 0xFF use the current behavior - other run_groups do not wait for them, and they do not wait for anything else (other than their own shouldRun()).  \r\n\r\nExcluding that exception, groups of Tasks will execute in the order of their run_group_id in a cycle.  Higher numbered RunGroups will delay until all Tasks in the lower RunGroups have marked themselves complete.  \r\n\r\nWhen all the Tasks in all RunGroups have executed, the cycle_id number increments and the Tasks' completed state get reset.\r\n\r\nHere's an example from 3 Tasks to explain it more clearly.\r\n2 Tasks \"Acquire\" data then 1 \"Processes\" it.\r\n\r\nNotice the cycle_id (the first number) remains the same until all three Tasks have run and that the two \"Acquiring\" Tasks Finish before the \"Processing\" Task begins; and the cycle_id increments only after the \"Processing\" Task completes:\r\n```\r\n[Cycle_ID] ([Task_Id]) [Function]... [Internal Counter]\r\n2\t(1.1) Acquiring... 1\r\n2\t(1.2) Acquiring... 1\r\n2\t(1.2) Acquiring... 2\r\n2\t(1.1) Acquiring... 2\r\n2\t(1.2) Acquiring... 3\r\n2\t(1.2) Acquiring... 4\r\n2\t(1.1) Acquiring... 3\r\n2\t(1.2) Acquiring... 5\r\n2\t(1.2) Acquiring... 5 Done!\r\n2\t(1.1) Acquiring... 4\r\n2\t(1.1) Acquiring... 5\r\n2\t(1.1) Acquiring... 5 Done!\r\n2\t\t(5.1) Processing... 1\r\n2\t\t(5.1) Processing... 2\r\n2\t\t(5.1) Processing... 3\r\n2\t\t(5.1) Processing... 4\r\n2\t\t(5.1) Processing... 5\r\n2\t\t(5.1) Processing... 5 Done!\r\n3\t(1.1) Acquiring... 1\r\n3\t(1.2) Acquiring... 1\r\n3\t(1.2) Acquiring... 2\r\n3\t(1.2) Acquiring... 3\r\n3\t(1.1) Acquiring... 2\r\n3\t(1.2) Acquiring... 4\r\n3\t(1.1) Acquiring... 3\r\n3\t(1.2) Acquiring... 5\r\n3\t(1.2) Acquiring... 5 Done!\r\n3\t(1.1) Acquiring... 4\r\n3\t(1.1) Acquiring... 5\r\n3\t(1.1) Acquiring... 5 Done!\r\n3\t\t(5.1) Processing... 1\r\n3\t\t(5.1) Processing... 2\r\n3\t\t(5.1) Processing... 3\r\n3\t\t(5.1) Processing... 4\r\n3\t\t(5.1) Processing... 5\r\n3\t\t(5.1) Processing... 5 Done!\r\n4\t(1.1) Acquiring... 1\r\n4\t(1.2) Acquiring... 1\r\n4\t(1.2) Acquiring... 2\r\n4\t(1.2) Acquiring... 3\r\n4\t(1.1) Acquiring... 2\r\n4\t(1.2) Acquiring... 4\r\n4\t(1.2) Acquiring... 5\r\n4\t(1.2) Acquiring... 5 Done!\r\n4\t(1.1) Acquiring... 3\r\n```\r\n\r\nIn this example, no Task had run_group_id 0xFF because its output more cluttered and confused the example than helped it.\r\n\r\nIf there's interest, I can explain the internals of how it works a bit more; and discuss modifications for merging it.  While it's not \"trivial\", it's not a big diff.\r\n\r\nOne of the things that I put a lot of time into is keeping the amount of state required to as little as I could imagine (but no less :) ) and making the update functions as efficient/quick as I could (for instance, the new code never makes a run through the entire list of Tasks; it does it all \"inline\" as the Scheduler reaches them).\r\n\r\nThe SchedulerClass added 4 bytes; and the Tasks 2 bytes and 2 bools.\r\n\r\n\r\nOne thing I wanted that I couldn't do easily was get \"loop()\" to return a bool rather than a void.\r\nI think that would make things cleaner/clearer to understand.\r\n\r\nThanks!"},"comment":{"url":"https://api.github.com/repos/nrwiersma/ESP8266Scheduler/issues/comments/260212824","html_url":"https://github.com/nrwiersma/ESP8266Scheduler/issues/3#issuecomment-260212824","issue_url":"https://api.github.com/repos/nrwiersma/ESP8266Scheduler/issues/3","id":260212824,"user":{"login":"MikeFair","id":2304557,"avatar_url":"https://avatars.githubusercontent.com/u/2304557?v=3","gravatar_id":"","url":"https://api.github.com/users/MikeFair","html_url":"https://github.com/MikeFair","followers_url":"https://api.github.com/users/MikeFair/followers","following_url":"https://api.github.com/users/MikeFair/following{/other_user}","gists_url":"https://api.github.com/users/MikeFair/gists{/gist_id}","starred_url":"https://api.github.com/users/MikeFair/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/MikeFair/subscriptions","organizations_url":"https://api.github.com/users/MikeFair/orgs","repos_url":"https://api.github.com/users/MikeFair/repos","events_url":"https://api.github.com/users/MikeFair/events{/privacy}","received_events_url":"https://api.github.com/users/MikeFair/received_events","type":"User","site_admin":false},"created_at":"2016-11-13T21:05:26Z","updated_at":"2016-11-13T21:05:26Z","body":"```\r\nvoid loopWrapper() {\r\n    if (!setup_done) {\r\n        setup();\r\n        setup_done = true;\r\n    }\r\n\r\n    while(1) {\r\n        this->loop_complete = loop();\r\n        yield();\r\n    }\r\n}\r\n```\r\n\r\nExactly!\r\nAnd perhaps the group update functions should go there too.  \r\nCurrently they execute anytime the Task runs delay or yield too; which creates a side effect when loop_complete = true happens before a delay(); the loop() would \"pause\" and then pick up again just after the delay() on the next cycle.  Which makes perfect sense for what/how it's working; just not what I originally intended.\r\n\r\n----\r\n\r\n> Perhaps some of the reset logic for the current_cycle_id and loop_complete could also be internalized into the task. Perhaps being a reset() and update() function. This may take some of the burden away from the Scheduler.\r\n\r\nI'm not opposed to the idea but I'm not sure it would work. \r\nI in fact actually started out by putting it in the Tasks following the Delay pattern; the problem was Task.h cannot see Scheduler.h to call any functions the SchedulerClass exported (like getCurrentCycleID()) and I didn't want to mess with that; I also saw that SchedulerClass was already a friend to Task so could mess with the internals of Task if it needed to; but not the other way around.  It also turned out the Task update code reduced to a couple if tests and a couple assignments in a function that already had to look at the internal state information of the Task anyway; so updating that information \"while it was there\" was an extremely \"light\" addition...\r\n\r\nI thought briefly about separating the state and logic out into a new RunGroups.h that they both included; again that seemed too heavy handed to keep the feature \"light\" on the resources (though I honestly don't think it would actually be any more \"expensive\" than the current method and it would make a good \"extension pattern\" that could be followed by others so would be worth doing)...\r\n\r\nTo be as open/forthcoming as I can see about it; there are two things a Task could manage itself:\r\n```\r\n  // (1)\r\n  if (scheduler_cycle_id != current->current_cycle_id) {\r\n    if (task_run_group_id != 0xFF) current->loop_complete = false;\r\n    current->current_cycle_id = scheduler_cycle_id;\r\n  }\r\n\r\n  // and (2) psuedocode\r\n  if (the_tasks_run_group_is_an_active_group) {\r\n    current->run_group_active = true;\r\n  } else {\r\n    current->run_group_active = false;\r\n  }\r\n```\r\n\r\nIn other words, (1) if the Scheduler's Cycle has moved from what the Task remembers; that's the signal to reset the loop_complete flag; and (2) the Scheduler indicates to the Task whether or not it's run_group is currently active by setting the flag.  \r\n\r\nThe Task then tests those flags in shouldRun().\r\n\r\nEverything else would almost certainly be calling a function the SchedulerClass exported because that's where the group state is (because Tasks can't see information about other Tasks).\r\n\r\n\r\nAnd let's keep in mind too regarding the term \"burden\"; it's going to be the same CPU cycles spent to do the work whether it happens in the Scheduler or in the Tasks, regardless of where it happens, because the work must be done.  The key point more important than \"who\" does the work is maximizing time spent in \"loop()\" (by minimizing CPU cycles other places); a Task's \"shouldRun()\" is not \"loop()\" and so therefore is as equally costly to \"loop()\" as the Scheduler is; I'd even argue that a Task's \"shouldRun()\" is an externalized extension of the Scheduler. :)\r\n\r\n"}}
{"actor":{"display_login":"MikeFair"}}