From fff36010415ee91894708bd5b17329469f5bc53a Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Sun, 25 Feb 2024 00:00:51 -0500 Subject: [PATCH] Working on 16.0.0 (#53) * Fixed spelling, started ideaing for 16.0.0 * Updated files * Updated readme * Updated version * Concat conns now properly transfer events * Testing types * Connections can be % with functions * Updated connections * Fixed issue with double thread activations (Looking for another solution) * Working on issue with love threaded functions not waiting when in a thread * Working on issue where threads created in threads don't work * Fixed broken threads for love * Fixed some issues with threads * removed test * Updated changes.md * Plan on testing parity between the threading modules * Writing tests for system threading * Added test cases for threading, fixed issues. Todo test love2d * Fixed love2d to succeed with tests * All tests working * Updated files for testing * Modified tests to make it more seamless * removed extra __cores in lanes/pseudo * Working on new priority scheme * Working on priority management * Working on custom prioritySchemes * Fixed issues with missing code * Threaded processors * THREAD.exposeENV(), thread:newProcessor() * Typo in changes.md * Fixed typo in pseudoManager * fixing * Trying to fix exposeENV with pseudoThreading * Changes to threads * updated changes.md * Working on systemthreadedprocess, and experimental newProxy for threading * newProxy and STP work * newProxy implemented * Proxies work with connections now :D * Added tstep to STP, updated changes.md * thread.hold(proxy.conn) * Clean up connection events when holding, working on scheduling tasks/threads to system threaded processors * Getting loads of processors implemented * Finished getLoad(type) * Fixed some bugs * Added an easy way to share a table, found some limitations with lanes threading. * THREAD_NAME set for main thread, connections break the rules for proxies * Testing * Really close to portable proxies, currently extreamly unstable! * Debugging what is going on... * Fixed critical issue with coroutine based threads * Removed extra bloat, proxies are portable now! * Started work on the debugManager * Testing actions, fixing bugs with lanes * Testing... * fixing actions * typo fixed * Throw an error when things break * fixing stuff * Fixed issue with errors not going through * Removed system threaded connections, soon to be replaced by proxies * Testing love2d tests * Test love2d * Use later love-build * Use ubuntu for build * Fixed path * Use appimage * use sudo * No window for love2d * Fixed love2d tests * Testing love2d * Use workspace * Moved other tests while testing * actually pull the repo * packagepath set * Fixed pull * Update multi * Removed link * Edited symlink * Added timeout to build * Rewriting loveManager, too much outdated code * Still implementing new love2d threading code * Rewriting love2d threading binding * Working on adding a Hold method to all objects. Will document how they all work when done. * jobqueues having isues with stp * new pack/unpack for tables, current issue is things being turned into strings * Fixed packing of values into threads, need to fix system proxies and system threaded processors * testing... * Not hard crashing when error is encountered * Should now push non 0 exit codes * Push error when an error happens * Closer to getting things working... * Working on new type system, planning out debugmanager * Fixed error code issue * Test for 5.1 * Planning out debugManager * Some work on the debug manager, proxies working on lanes, todo get pseudo manager and love2d working * Working on getting pseudoThreading tests to work * Added function / connection * Added boost method * Document new features to conns, todo fix newTask * Fixed newTask() * Updated changes.md and fixed some bugs * Added thread.defer(func) * Fixed tests on lanes and pseudo threading, todo fix love2d threading * Fixed paths * Working on paths * Testing paths * Add test for rockspec * Fixed issues * Fixed typo * Added test for defer * Threading working in love2d * Fixed, conf * lanes uses a threaded function like waitfor function * Cleaned up changes.md * added priorityManager to rockspec --- .github/workflows/love.yml | 25 + .github/workflows/nix_ci.yml | 41 + .gitignore | 3 +- NetworkManager.md | 26 + README.md | 29 +- docs/changes.md | 580 ++++++- init.lua | 1485 ++++++++++------- integration/debugManager/init.lua | 106 ++ integration/effilManager/extensions.lua | 0 integration/effilManager/init.lua | 46 + integration/effilManager/threads.lua | 0 integration/lanesManager/extensions.lua | 163 +- integration/lanesManager/init.lua | 89 +- integration/lanesManager/threads.lua | 57 +- integration/loveManager/extensions.lua | 349 ++-- integration/loveManager/init.lua | 211 +-- integration/loveManager/threads.lua | 352 ++-- integration/lovrManager/extensions.lua | 9 +- integration/lovrManager/init.lua | 17 +- integration/lovrManager/threads.lua | 4 +- integration/luvitManager.lua | 4 +- integration/pesudoManager/extensions.lua | 141 -- integration/priorityManager/init.lua | 213 +++ integration/pseudoManager/extensions.lua | 221 +++ .../{pesudoManager => pseudoManager}/init.lua | 56 +- .../threads.lua | 44 +- integration/sharedExtensions/init.lua | 336 ++++ lovethreads/conf.lua | 20 + lovethreads/main.lua | 75 + lovethreads/multi | 1 + rockspecs/multi-16.0-0.rockspec | 42 + tests/conf.lua | 39 + tests/main.lua | 10 + tests/multi | 1 + tests/runtests.lua | 376 +++-- tests/test.lua | 431 +++-- tests/threadtests.lua | 246 +++ tests/vscode-debuggee.lua | 1102 ++++++++++++ 38 files changed, 5241 insertions(+), 1709 deletions(-) create mode 100644 .github/workflows/love.yml create mode 100644 .github/workflows/nix_ci.yml create mode 100644 NetworkManager.md create mode 100644 integration/debugManager/init.lua create mode 100644 integration/effilManager/extensions.lua create mode 100644 integration/effilManager/init.lua create mode 100644 integration/effilManager/threads.lua delete mode 100644 integration/pesudoManager/extensions.lua create mode 100644 integration/priorityManager/init.lua create mode 100644 integration/pseudoManager/extensions.lua rename integration/{pesudoManager => pseudoManager}/init.lua (70%) rename integration/{pesudoManager => pseudoManager}/threads.lua (81%) create mode 100644 integration/sharedExtensions/init.lua create mode 100644 lovethreads/conf.lua create mode 100644 lovethreads/main.lua create mode 100644 lovethreads/multi create mode 100644 rockspecs/multi-16.0-0.rockspec create mode 100644 tests/conf.lua create mode 100644 tests/main.lua create mode 120000 tests/multi create mode 100644 tests/threadtests.lua create mode 100644 tests/vscode-debuggee.lua diff --git a/.github/workflows/love.yml b/.github/workflows/love.yml new file mode 100644 index 0000000..d38888d --- /dev/null +++ b/.github/workflows/love.yml @@ -0,0 +1,25 @@ +name: Build & Run tests Love2d + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + strategy: + fail-fast: false + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - name: Install love2d + run: | + sudo apt install fuse + wget https://github.com/love2d/love/releases/download/11.4/love-11.4-x86_64.AppImage -O love.AppImage + sudo chmod +x love.AppImage + - name: Run Tests + run: | + ./love.AppImage tests \ No newline at end of file diff --git a/.github/workflows/nix_ci.yml b/.github/workflows/nix_ci.yml new file mode 100644 index 0000000..fd6b9f0 --- /dev/null +++ b/.github/workflows/nix_ci.yml @@ -0,0 +1,41 @@ +name: Build & Run tests Ubuntu + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + strategy: + fail-fast: false + matrix: + build-type: [Release] # Debug + lua: ["lua 5.1", "lua 5.2", "lua 5.3", "lua 5.4", "luajit 2.1.0-beta3"] + os: ["ubuntu-latest"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + + - uses: actions/setup-python@v2 + with: + python-version: '3.10' + + - name: Setup env + run: | + pip install hererocks + hererocks lua-pkg --${{ matrix.lua }} -rlatest + + - name: Install lanes and multi + run: | + source ${{github.workspace}}/lua-pkg/bin/activate + luarocks install lanes + luarocks install rockspecs/multi-16.0-0.rockspec + + - name: Run Tests + run: | + source ${{github.workspace}}/lua-pkg/bin/activate + lua tests/runtests.lua diff --git a/.gitignore b/.gitignore index f3ede8b..0a96143 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.code-workspace -lua5.4/* \ No newline at end of file +lua5.4/* +test.lua \ No newline at end of file diff --git a/NetworkManager.md b/NetworkManager.md new file mode 100644 index 0000000..3b40295 --- /dev/null +++ b/NetworkManager.md @@ -0,0 +1,26 @@ +# NTHREAD Namespace +- [ ] NTHREAD.set(name, val) +- [ ] NTHREAD.get(name, val) +- [ ] NTHREAD.waitFor(name) +- [ ] NTHREAD.getCores()* +- [ ] NTHREAD.getConsole() +- [ ] NTHREAD.getThreads() +- [ ] NTHREAD.kill() +- [ ] NTHREAD.getName() +- [ ] NTHREAD.getID() +- [ ] NTHREAD.pushStatus(...) +- [ ] NTHREAD.sleep(n) +- [ ] NTHREAD.hold(n) +- [ ] NTHREAD.setENV(env) +- [ ] NTHREAD.getENV() + +# Extensions +- [ ] multi:newNetworkThreadedQueue(name) +- [ ] multi:newNetworkThreadedTable(name) +- [ ] multi:newNetworkThreadedJobQueue(n) +- [ ] multi:newNetworkThreadedConnection(name) + +# Core +- [ ] NTHREAD:newFunction(func, holdme) +- [ ] NTHREAD:newNetworkThread(name, func, ...) +- [ ] mulit:newNetworkThread(name, func, ...) \ No newline at end of file diff --git a/README.md b/README.md index f467bd2..01235d1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ -# Multi Version: 15.3.1 A world of Connections +# Multi Version: 16.0.0 - Connecting the dots **Key Changes** -- SystemThreadedConnections -- Restructured the directory structure of the repo (Allows for keeping multi as a submodule and being able to require it as is) +- Expanded connection logic +- New integration priorityManager +- Tests for threads +- Consistent behavior between the threading integrations +- Improved love2d threading - Bug fixes Found an issue? Please [submit it](https://github.com/rayaman/multi/issues) and someone will look into it! @@ -18,9 +21,19 @@ Progress is being made in [v16.0.0](https://github.com/rayaman/multi/tree/v16.0. INSTALLING ---------- Link to optional dependencies: -- [lanes](https://github.com/LuaLanes/lanes) +- [lanes](https://github.com/LuaLanes/lanes) `luarocks install lanes` + +- [chronos](https://github.com/ldrumm/chronos) `luarocks install chronos` - [love2d](https://love2d.org/) + + When using love2d add multi:uManager() or any processor to love.update() + + ```lua + function love.update(dt) + multi:uManager() + end + ``` To install copy the multi folder into your environment and you are good to go
If you want to use the system threads, then you'll need to install lanes or love2d game engine! @@ -36,12 +49,18 @@ https://discord.gg/U8UspuA Planned features/TODO --------------------- -- [ ] Create test suite (In progress, mostly done) +- [x] ~~Create test suite (In progress, mostly done)~~ - [ ] Network Parallelism rework Usage: [Check out the documentation for more info](https://github.com/rayaman/multi/blob/master/Documentation.md) ----- +You can run tests in 2 ways: +``` +lua tests/runtests.lua (Runs all tests, attempts to use lanes) +love tests (Runs all tests in love2d env) +``` + ```lua local multi, thread = require("multi"):init() GLOBAL, THREAD = require("multi.integration.threading"):init() diff --git a/docs/changes.md b/docs/changes.md index 6ae155b..9ea6f80 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -1,10 +1,584 @@ # Changelog Table of contents --- +[Update 16.0.0 - Connecting the dots](#update-1600---getting-the-priorities-straight)
[Update 15.3.1 - Bug fix](#update-1531---bug-fix)
[Update 15.3.0 - A world of connections](#update-1530---a-world-of-connections)
[Update 15.2.1 - Bug fix](#update-1521---bug-fix)
-[Update 15.2.0 - Upgrade Complete](#update-1520---upgrade-complete)
[Update 15.1.0 - Hold the thread!](#update-1510---hold-the-thread)
[Update 15.0.0 - The art of faking it](#update-1500---the-art-of-faking-it)
[Update 14.2.0 - Bloatware Removed](#update-1420---bloatware-removed)
[Update 14.1.0 - A whole new world of possibilities](#update-1410---a-whole-new-world-of-possibilities)
[Update 14.0.0 - Consistency, Additions and Stability](#update-1400---consistency-additions-and-stability)
[Update 13.1.0 - Bug fixes and features added](#update-1310---bug-fixes-and-features-added)
[Update 13.0.0 - Added some documentation, and some new features too check it out!](#update-1300---added-some-documentation-and-some-new-features-too-check-it-out)
[Update 12.2.2 - Time for some more bug fixes!](#update-1222---time-for-some-more-bug-fixes)
[Update 12.2.1 - Time for some bug fixes!](#update-1221---time-for-some-bug-fixes)
[Update 12.2.0 - The chains of binding](#update-1220---the-chains-of-binding)
[Update 12.1.0 - Threads just can't hold on anymore](#update-1210---threads-just-cant-hold-on-anymore)
[Update: 12.0.0 - Big update (Lots of additions some changes)](#update-1200---big-update-lots-of-additions-some-changes)
[Update: 1.11.1 - Small Clarification on Love](#update-1111---small-clarification-on-love)
[Update: 1.11.0](#update-1110)
[Update: 1.10.0](#update-1100)
[Update: 1.9.2](#update-192)
[Update: 1.9.1 - Threads can now argue](#update-191---threads-can-now-argue)
[Update: 1.9.0](#update-190)
[Update: 1.8.7](#update-187)
[Update: 1.8.6](#update-186)
[Update: 1.8.5](#update-185)
[Update: 1.8.4](#update-184)
[Update: 1.8.3 - Mainloop recieves some needed overhauling](#update-183---mainloop-recieves-some-needed-overhauling)
[Update: 1.8.2](#update-182)
[Update: 1.8.1](#update-181)
[Update: 1.7.6](#update-176)
[Update: 1.7.5](#update-175)
[Update: 1.7.4](#update-174)
[Update: 1.7.3](#update-173)
[Update: 1.7.2](#update-172)
[Update: 1.7.1 - Bug Fixes Only](#update-171---bug-fixes-only)
[Update: 1.7.0 - Threading the systems](#update-170---threading-the-systems)
[Update: 1.6.0](#update-160)
[Update: 1.5.0](#update-150)
[Update: 1.4.1 (4/10/2017) - First Public release of the library](#update-141-4102017---first-public-release-of-the-library)
[Update: 1.4.0 (3/20/2017)](#update-140-3202017)
[Update: 1.3.0 (1/29/2017)](#update-130-1292017)
[Update: 1.2.0 (12.31.2016)](#update-120-12312016)
[Update: 1.1.0](#update-110)
[Update: 1.0.0](#update-100)
[Update: 0.6.3](#update-063)
[Update: 0.6.2](#update-062)
[Update: 0.6.1-6](#update-061-6)
[Update: 0.5.1-6](#update-051-6)
[Update: 0.4.1](#update-041)
[Update: 0.3.0 - The update that started it all](#update-030---the-update-that-started-it-all)
[Update: EventManager 2.0.0](#update-eventmanager-200)
[Update: EventManager 1.2.0](#update-eventmanager-120)
[Update: EventManager 1.1.0](#update-eventmanager-110)
[Update: EventManager 1.0.0 - Error checking](#update-eventmanager-100---error-checking)
[Version: EventManager 0.0.1 - In The Beginning things were very different](#version-eventmanager-001---in-the-beginning-things-were-very-different) +[Update 15.2.0 - Upgrade Complete](#update-1520---upgrade-complete)
+[Update 15.1.0 - Hold the thread!](#update-1510---hold-the-thread)
+[Update 15.0.0 - The art of faking it](#update-1500---the-art-of-faking-it)
+[Update 14.2.0 - Bloatware Removed](#update-1420---bloatware-removed)
+[Update 14.1.0 - A whole new world of possibilities](#update-1410---a-whole-new-world-of-possibilities)
+[Update 14.0.0 - Consistency, Additions and Stability](#update-1400---consistency-additions-and-stability)
+[Update 13.1.0 - Bug fixes and features added](#update-1310---bug-fixes-and-features-added)
+[Update 13.0.0 - Added some documentation, and some new features too check it out!](#update-1300---added-some-documentation-and-some-new-features-too-check-it-out)
+[Update 12.2.2 - Time for some more bug fixes!](#update-1222---time-for-some-more-bug-fixes)
+[Update 12.2.1 - Time for some bug fixes!](#update-1221---time-for-some-bug-fixes)
+[Update 12.2.0 - The chains of binding](#update-1220---the-chains-of-binding)
+[Update 12.1.0 - Threads just can't hold on anymore](#update-1210---threads-just-cant-hold-on-anymore)
+[Update: 12.0.0 - Big update (Lots of additions some changes)](#update-1200---big-update-lots-of-additions-some-changes)
+[Update: 1.11.1 - Small Clarification on Love](#update-1111---small-clarification-on-love)
+[Update: 1.11.0](#update-1110)
+[Update: 1.10.0](#update-1100)
+[Update: 1.9.2](#update-192)
+[Update: 1.9.1 - Threads can now argue](#update-191---threads-can-now-argue)
+[Update: 1.9.0](#update-190)
+[Update: 1.8.7](#update-187)
+[Update: 1.8.6](#update-186)
+[Update: 1.8.5](#update-185)
+[Update: 1.8.4](#update-184)
+[Update: 1.8.3 - Mainloop recieves some needed overhauling](#update-183---mainloop-recieves-some-needed-overhauling)
+[Update: 1.8.2](#update-182)
+[Update: 1.8.1](#update-181)
+[Update: 1.7.6](#update-176)
+[Update: 1.7.5](#update-175)
+[Update: 1.7.4](#update-174)
+[Update: 1.7.3](#update-173)
+[Update: 1.7.2](#update-172)
+[Update: 1.7.1 - Bug Fixes Only](#update-171---bug-fixes-only)
+[Update: 1.7.0 - Threading the systems](#update-170---threading-the-systems)
+[Update: 1.6.0](#update-160)
+[Update: 1.5.0](#update-150)
+[Update: 1.4.1 (4/10/2017) - First Public release of the library](#update-141-4102017---first-public-release-of-the-library)
+[Update: 1.4.0 (3/20/2017)](#update-140-3202017)
+[Update: 1.3.0 (1/29/2017)](#update-130-1292017)
+[Update: 1.2.0 (12.31.2016)](#update-120-12312016)
+[Update: 1.1.0](#update-110)
+[Update: 1.0.0](#update-100)
+[Update: 0.6.3](#update-063)
+[Update: 0.6.2](#update-062)
+[Update: 0.6.1-6](#update-061-6)
+[Update: 0.5.1-6](#update-051-6)
+[Update: 0.4.1](#update-041)
+[Update: 0.3.0 - The update that started it all](#update-030---the-update-that-started-it-all)
+[Update: EventManager 2.0.0](#update-eventmanager-200)
+[Update: EventManager 1.2.0](#update-eventmanager-120)
+[Update: EventManager 1.1.0](#update-eventmanager-110)
+[Update: EventManager 1.0.0 - Error checking](#update-eventmanager-100---error-checking)
+[Version: EventManager 0.0.1 - In The Beginning things were very different](#version-eventmanager-001---in-the-beginning-things-were-very-different) + +# Update 16.0.0 - Getting the priorities straight + +## Added New Integration: **priorityManager** + +Allows the user to have multi auto set priorities (Requires chronos). Also adds the functionality to create your own runners (multi:mainloop(), multi:umanager()) that you can set using the priority manager. Even if you do not have `chronos` installed all other features will still work! +- Allows the creation of custom priorityManagers + +Added +--- +- thread.defer(func) -- When using a co-routine thread or co-routine threaded function, defer will call it's function at the end of the the threads life through normal execution or an error. In the case of a threaded function, when the function returns or errors. +- multi:setTaskDelay(delay), Tasks which are now tied to a processor can have an optional delay between the execution between each task. Useful perhaps for rate limiting. Without a delay all grouped tasks will be handled in one step. `delay` can be a function as well and will be processed as if thread.hold was called. +- processor's now have a boost function which causes it to run its processes the number of times specified in the `boost(count)` function +- thread.hold will now use a custom hold method for objects with a `Hold` method. This is called like `obj:Hold(opt)`. The only argument passed is the optional options table that thread.hold can pass. There is an exception for connection objects. While they do contain a Hold method, the Hold method isn't used and is there for proxy objects, though they can be used in non proxy/thread situations. Hold returns all the arguments that the connection object was fired with. +- shared_table = STP:newSharedTable(tbl_name) -- Allows you to create a shared table that all system threads in a process have access to. Returns a reference to that table for use on the main thread. Sets `_G[tbl_name]` on the system threads so you can access it there. + ```lua + package.path = "?/init.lua;?.lua;"..package.path + + multi, thread = require("multi"):init({print=true}) + THREAD, GLOBAL = require("multi.integration.lanesManager"):init() + + stp = multi:newSystemThreadedProcessor(8) + + local shared = stp:newSharedTable("shared") + + shared["test"] = "We work!" + + for i=1,5 do + -- There is a bit of overhead when creating threads on a process. Takes some time, mainly because we are creating a proxy. + stp:newThread(function() + local multi, thread = require("multi"):init() + local shared = _G["shared"] + print(THREAD_NAME, shared.test, shared.test2) + multi:newAlarm(.5):OnRing(function() -- Play around with the time. System threads do not create instantly. They take quite a bit of time to get spawned. + print(THREAD_NAME, shared.test, shared.test2) + end) + end) + end + + shared["test2"] = "We work!!!" + + multi:mainloop() + ``` + + Output: + ``` + INFO: Integrated Lanes Threading! + STJQ_cPXT8GOx We work! nil + STJQ_hmzdYDVr We work! nil + STJQ_3lwMhnfX We work! nil + STJQ_hmzdYDVr We work! nil + STJQ_cPXT8GOx We work! nil + STJQ_cPXT8GOx We work! We work!!! + STJQ_hmzdYDVr We work! We work!!! + STJQ_3lwMhnfX We work! We work!!! + STJQ_hmzdYDVr We work! We work!!! + STJQ_cPXT8GOx We work! We work!!! + ``` + +- multi:chop(obj) -- We cannot directly interact with a local object on lanes, so we chop the object and set some globals on the thread side. Should use like: `mulit:newProxy(multi:chop(multi:newThread(function() ... end)))` +- multi:newProxy(ChoppedObject) -- Creates a proxy object that allows you to interact with an object on a thread + + **Note:** Objects with __index=table do not work with the proxy object! The object must have that function in it's own table for proxy to pick it up and have it work properly. Connections on a proxy allow you to subscribe to an event on the thread side of things. The function that is being connected to happens on the thread! +- multi:newSystemThreadedProcessor(name) -- Works like newProcessor(name) each object created returns a proxy object that you can use to interact with the objects on the system thread + ```lua + package.path = "?/init.lua;?.lua;"..package.path + + multi, thread = require("multi"):init({print=true}) + THREAD, GLOBAL = require("multi.integration.lanesManager"):init() + + stp = multi:newSystemThreadedProcessor("Test STP") + + alarm = stp:newAlarm(3) + + alarm._OnRing:Connect(function(alarm) + print("Hmm...", THREAD_NAME) + end) + ``` + Output: + ``` + Hmm... SystemThreadedJobQueue_A5tp + ``` + Internally the SystemThreadedProcessor uses a JobQueue to handle things. The proxy function allows you to interact with these objects as if they were on the main thread, though there actions are carried out on the main thread. + + Proxies can also be shared between threads, just remember to use proxy:getTransferable() before transferring and proxy:init() on the other end. (We need to avoid copying over coroutines) + + The work done with proxies negates the usage of multi:newSystemThreadedConnection(), the only difference is you lose the metatables from connections. + + You cannot connect directly to a proxy connection on the non proxy thread, you can however use proxy_conn:Hold() or thread.hold(proxy_conn) to emulate this, see below. + + ```lua + package.path = "?/init.lua;?.lua;"..package.path + + multi, thread = require("multi"):init({print=true, warn=true, error=true}) + THREAD, GLOBAL = require("multi.integration.lanesManager"):init() + + stp = multi:newSystemThreadedProcessor(8) + + tloop = stp:newTLoop(nil, 1) + + multi:newSystemThread("Testing proxy copy",function(tloop) + local function tprint (tbl, indent) + if not indent then indent = 0 end + for k, v in pairs(tbl) do + formatting = string.rep(" ", indent) .. k .. ": " + if type(v) == "table" then + print(formatting) + tprint(v, indent+1) + else + print(formatting .. tostring(v)) + end + end + end + local multi, thread = require("multi"):init() + tloop = tloop:init() + print("tloop type:",tloop.Type) + print("Testing proxies on other threads") + thread:newThread(function() + while true do + thread.hold(tloop.OnLoop) + print(THREAD_NAME,"Loopy") + end + end) + tloop.OnLoop(function(a) + print(THREAD_NAME, "Got loop...") + end) + multi:mainloop() + end, tloop:getTransferable()).OnError(multi.error) + + print("tloop", tloop.Type) + + thread:newThread(function() + print("Holding...") + thread.hold(tloop.OnLoop) + print("Held on proxied no proxy connection 1") + end).OnError(print) + + thread:newThread(function() + tloop.OnLoop:Hold() + print("held on proxied no proxy connection 2") + end) + + tloop.OnLoop(function() + print("OnLoop",THREAD_NAME) + end) + + thread:newThread(function() + while true do + tloop.OnLoop:Hold() + print("OnLoop",THREAD_NAME) + end + end).OnError(multi.error) + + multi:mainloop() + ``` + Output: + ``` + INFO: Integrated Lanes Threading! 1 + tloop proxy + Holding... + tloop type: proxy + Testing proxies on other threads + OnLoop STJQ_W9SZGB6Y + STJQ_W9SZGB6Y Got loop... + OnLoop MAIN_THREAD + Testing proxy copy Loopy + Held on proxied no proxy connection 1 + held on proxied no proxy connection 2 + OnLoop STJQ_W9SZGB6Y + STJQ_W9SZGB6Y Got loop... + Testing proxy copy Loopy + OnLoop MAIN_THREAD + OnLoop STJQ_W9SZGB6Y + STJQ_W9SZGB6Y Got loop... + + ... (Will repeat every second) + + Testing proxy copy Loopy + OnLoop MAIN_THREAD + OnLoop STJQ_W9SZGB6Y + STJQ_W9SZGB6Y Got loop... + + ... + ``` + + The proxy version can only subscribe to events on the proxy thread, which means that connection metamethods will not work with the proxy version (`_OnRing` on the non proxy thread side), but the (`OnRing`) version will work. Cleverly handling the proxy thread and the non proxy thread will allow powerful connection logic. Also this is not a full system threaded connection. **Proxies should only be used between 2 threads!** To keep things fast I'm using simple queues to transfer data. There is no guarantee that things will work! + + Currently supporting: + - proxyLoop = STP:newLoop(...) + - proxyTLoop = STP:newTLoop(...) + - proxyUpdater = STP:newUpdater(...) + - proxyEvent = STP:newEvent(...) + - proxyAlarm = STP:newAlarm(...) + - proxyStep = STP:newStep(...) + - proxyTStep = STP:newTStep(...) + - proxyThread = STP:newThread(...) + - proxyService = STP:newService(...) + - threadedFunction = STP:newFunction(...) + + Unique: + - STP:newSharedTable(name) + +
+ + **STP** functions (The ones above) cannot be called within coroutine based thread when using lanes. This causes thread.hold to break. Objects(proxies) returned by these functions are ok to use in coroutine based threads! + ```lua + package.path = "?/init.lua;?.lua;"..package.path + + multi, thread = require("multi"):init({print=true}) + THREAD, GLOBAL = require("multi.integration.lanesManager"):init() + + stp = multi:newSystemThreadedProcessor() + + alarm = stp:newAlarm(3) + + alarm.OnRing:Connect(function(alarm) + print("Hmm...", THREAD_NAME) + end) + + thread:newThread(function() + print("Holding...") + local a = thread.hold(alarm.OnRing) -- it works :D + print("We work!") + end) + + multi:mainloop() + ``` + +- multi.OnObjectDestroyed(func(obj, process)) now supplies obj, process just like OnObjectCreated +- thread:newProcessor(name) -- works mostly like a normal process, but all objects are wrapped within a thread. So if you create a few loops, you can use thread.hold() call threaded functions and wait and use all features that using coroutines provide. +- multi.Processors:getHandler() -- returns the thread handler for a process +- multi.OnPriorityChanged(self, priority) -- Connection is triggered whenever the priority of an object is changed! +- multi.setClock(clock_func) -- If you have access to a clock function that works like os.clock() you can set it using this function. The priorityManager if chronos is installed sets the clock to it's current version. +- multi:setCurrentTask() -- Used to set the current processor. Used in custom processors. +- multi:setCurrentProcess() -- Used to set the current processor. It should only be called on a processor object +- multi.success(...) -- Sends a success. Green `SUCCESS` mainly used for tests +- multi.warn(...) -- Sends a warning. Yellow `WARNING` +- multi.error(err) -- When called this function will gracefully kill multi, cleaning things up. Red `ERROR` + + **Note:** If you want to have multi.print, multi.warn and multi.error to work you need to enable them in settings + ```lua + multi, thread = require("multi"):init { + print=true, + warn=true, + error=true -- Errors will throw regardless. Setting to true will + -- cause the library to force hard crash itself! + } + ``` +- THREAD.exposeEnv(name) -- Merges set env into the global namespace of the system thread it was called in. +- THREAD.setENV(table [, name]) -- Set a simple table that will be merged into the global namespace. If a name is supplied the global namespace will not be merged. Call THREAD.exposeEnv(name) to expose that namespace within a thread. + + **Note:** To maintain compatibility between each integration use simple tables. No self references, and string indices only. + ```lua + THREAD.setENV({ + shared_function = function() + print("I am shared!") + end + }) + ``` + When this function is used it writes to a special variable that is read at thread spawn time. If this function is then ran later it can be used to set a different env and be applied to future spawned threads. +- THREAD.getENV() can be used to manage advanced uses of the setENV() functionality +- Connection objects now support the % function. This supports a function % connection object. What it does is allow you to **mod**ify the incoming arguments of a connection event. + ```lua + local conn1 = multi:newConnection() + local conn2 = function(a,b,c) return a*2, b*2, c*2 end % conn1 + conn2(function(a,b,c) + print("Conn2",a,b,c) + end) + conn1(function(a,b,c) + print("Conn1",a,b,c) + end) + conn1:Fire(1,2,3) + conn2:Fire(1,2,3) + ``` + Output: + ``` + Conn2 2 4 6 + Conn1 1 2 3 + Conn2 1 2 3 + ``` + **Note:** Conn1 does not get modified, however firing conn1 will also fire conn2 and have it's arguments modified. Also firing conn2 directly **does not** modify conn2's arguments! + See it's implementation below: + ```lua + __mod = function(obj1, obj2) + local cn = multi:newConnection() + if type(obj1) == "function" and type(obj2) == "table" then + obj2(function(...) + cn:Fire(obj1(...)) + end) + else + error("Invalid mod!", type(obj1), type(obj2),"Expected function, connection(table)") + end + return cn + end + ``` +- The len operator `#` will return the number of connections in the object! + ``` + local conn = multi:newConnection() + conn(function() print("Test 1") end) + conn(function() print("Test 2") end) + conn(function() print("Test 3") end) + conn(function() print("Test 4") end) + print(#conn) + ``` + Output: + ``` + 4 + ``` +- Connection objects can be negated -conn returns self so conn = -conn, reverses the order of connection events + ```lua + local conn = multi:newConnection() + conn(function() print("Test 1") end) + conn(function() print("Test 2") end) + conn(function() print("Test 3") end) + conn(function() print("Test 4") end) + + print("Fire 1") + conn:Fire() + conn = -conn + print("Fire 2") + conn:Fire() + ``` + Output: + ``` + Fire 1 + Test 1 + Test 2 + Test 3 + Test 4 + Fire 2 + Test 4 + Test 3 + Test 2 + Test 1 + ``` +- Connection objects can be divided, function / connection + This is a mix between the behavior between mod and concat, where the original connection can forward it's events to the new one as well as do a check like concat can. View it's implementation below: + ```lua + __div = function(obj1, obj2) -- / + local cn = self:newConnection() + local ref + if type(obj1) == "function" and type(obj2) == "table" then + obj2(function(...) + local args = {obj1(...)} + if args[1] then + cn:Fire(multi.unpack(args)) + end + end) + else + multi.error("Invalid divide! ", type(obj1), type(obj2)," Expected function/connection(table)") + end + return cn + end + ``` +- Connection objects can now be concatenated with functions, not each other. For example: + ```lua + multi, thread = require("multi"):init{print=true,findopt=true} + + local conn1, conn2 = multi:newConnection(), multi:newConnection() + conn3 = conn1 + conn2 + + conn1(function() + print("Hi 1") + end) + + conn2(function() + print("Hi 2") + end) + + conn3(function() + print("Hi 3") + end) + + function test(a,b,c) + print("I run before all and control if execution should continue!") + return a>b + end + + conn4 = test .. conn1 + + conn5 = conn2 .. function() print("I run after it all!") end + + conn4:Fire(3,2,3) + + -- This second one won't trigger the Hi's + conn4:Fire(1,2,3) + + conn5(function() + print("Test 1") + end) + + conn5(function() + print("Test 2") + end) + + conn5(function() + print("Test 3") + end) + + conn5:Fire() + ``` + + Output: + ``` + I run before all and control if things go! + Hi 3 + Hi 1 + Test 1 + Test 2 + Test 3 + I run after it all! + ``` + + **Note:** Concat of connections does modify internal events on both connections depending on the direction func .. conn or conn .. func See implemention below: + ```lua + __concat = function(obj1, obj2) + local cn = multi:newConnection() + local ref + if type(obj1) == "function" and type(obj2) == "table" then + cn(function(...) + if obj1(...) then + obj2:Fire(...) + end + end) + cn.__connectionAdded = function(conn, func) + cn:Unconnect(conn) + obj2:Connect(func) + end + elseif type(obj1) == "table" and type(obj2) == "function" then + ref = cn(function(...) + obj1:Fire(...) + obj2(...) + end) + cn.__connectionAdded = function() + cn.rawadd = true + cn:Unconnect(ref) + ref = cn(function(...) + if obj2(...) then + obj1:Fire(...) + end + end) + end + else + error("Invalid concat!", type(obj1), type(obj2),"Expected function/connection(table), connection(table)/function") + end + return cn + end + ``` + +Changed +--- +- multi:newTask(task) is not tied to the processor it is created on. +- `multi:getTasks()` renamed to `multi:getRunners()`, should help with confusion between multi:newTask() +- changed how multi adds unpack to the global namespace. Instead we capture that value into multi.unpack. +- multi:newUpdater(skip, func) -- Now accepts func as the second argument. So you don't need to call OnUpdate(func) after creation. +- multi errors now internally call `multi.error` instead of `multi.print` +- Actors Act() method now returns true when the main event is fired. Steps/Loops always return true. Nil is returned otherwise. +- Connection:Connect(func, name) Now you can supply a name and name the connection. +- Connection:getConnection(name) This will return the connection function which you can do what you will with it. +- Fast connections are the only connections. Legacy connections have been removed completely. Not much should change on the users end. Perhaps some minor changes. +- conn:Lock(conn) When supplied with a connection reference (What is returned by Connect(func)) it will only lock that connection Reference and not the entire connection. Calling without any arguments will lock the entire connection. +- connUnlock(conn) When supplied with a connection reference it restores that reference and it can be fired again. When no arguments are supplied it unlocks the entire connection. + + **Note:** Lock and Unlock when supplied with arguments and not supplied with arguments operate on different objects. If you unlock an entire connection. Individual connection refs will not unlock. The same applies with locking. The entire connection and references are treated differently. + +- multi.OnObjectCreated is only called when an object is created in a particular process. Proc.OnObjectCreated is needed to detect when an object is created within a process. +- multi.print shows "INFO" before it's message. Blue `INFO` +- Connections internals changed, not too much changed on the surface. +- newConnection(protect, func, kill) + - `protect` disables fastmode, but protects the connection + - `func` uses `..` and appends func to the connection so it calls it after all connections run. There is some internal overhead added when using this, but it isn't much. + - `kill` removes the connection when fired + + **Note:** When using protect/kill connections are triggered in reverse order + +Removed +--- +- multi.CONNECTOR_LINK -- No longer used +- multi:newConnector() -- No longer used +- THREAD.getName() use THREAD_NAME instead +- THREAD.getID() use THREAD_ID instead +- conn:SetHelper(func) -- With the removal of old Connect this function is no longer needed +- connection events can no longer can be chained with connect. Connect only takes a function that you want to connect + +Fixed +--- +- Issue with luajit w/5.2 compat breaking with coroutine.running(), fixed the script to properly handle so thread.isThread() returns as expected! +- Issue with coroutine based threads where they weren't all being scheduled due to a bad for loop. Replaced with a while to ensure all threads are consumed properly. If a thread created a thread that created a thread that may or may not be on the same process, things got messed up due to the original function not being built with these abstractions in mind. +- Issue with thread:newFunction() where a threaded function will keep a record of their returns and pass them to future calls of the function. +- Issue with multi:newTask(func) not properly handling tasks to be removed. Now uses a thread internally to manage things. +- multi.isMainThread was not properly handled in each integration. This has been resolved. +- Issue with pseudo threading env's being messed up. Required removal of getName and getID! +- connections being multiplied together would block the entire connection object from pushing events! This is not the desired effect I wanted. Now only the connection reference involved in the multiplication is locked! +- multi:reallocate(processor, index) has been fixed to work with the current changes of the library. +- Issue with lanes not handling errors properly. This is now resolved +- Oversight with how pushStatus worked with nesting threaded functions, connections and forwarding events. Changes made and this works now! + ```lua + func = thread:newFunction(function() + for i=1,10 do + thread.sleep(1) + thread.pushStatus(i) + end + end) + + func2 = thread:newFunction(function() + local ref = func() + ref.OnStatus(function(num) + -- do stuff with this data + + thread.pushStatus(num*2) -- Technically this is not ran within a thread. This is ran outside of a thread inside the thread handler. + end) + end) + + local handler = func2() + handler.OnStatus(function(num) + print(num) + end) + ``` + +ToDo +--- +- Network Manager, I know I said it will be in this release, but I'm still planning it out. # Update 15.3.1 - Bug fix Fixed @@ -23,7 +597,7 @@ conn2:Fire() -- Looks like this is triggering a response. It shouldn't. We need to account for this conn1:Fire() conn1:Fire() --- Triggering conn1 twice counted as a valid way to trigger the phantom connection (conn1 * conn2) +-- Triggering conn1 twice counted as a valid way to trigger the virtual connection (conn1 * conn2) -- Now in 15.3.1, this works properly and the above doesn't do anything. Internally connections are locked until the conditions are met. conn2:Fire() @@ -1705,7 +2279,7 @@ L: 2120906 I: 2120506 ``` -Auto Priority works by seeing what should be set high or low. Due to lua not having more persicion than milliseconds, I was unable to have a detailed manager that can set things to high, above normal, normal, ect. This has either high or low. If a process takes longer than .001 millisecond it will be set to low priority. You can change this by using the setting auto_lowest = multi.Priority_[PLevel] the defualt is low, not idle, since idle tends to get about 1 process each second though you can change it to idle using that setting. +Auto Priority works by seeing what should be set high or low. Due to lua not having more persicion than milliseconds, I was unable to have a detailed manager that can set things to high, above normal, normal, ect. This has either high or low. If a process takes longer than .001 millisecond it will be set to low priority. You can change this by using the setting auto_lowest = multi.Priority_[PLevel] the defualt is low, not idle, since idle tends to get about 1 process each second though you can change it to idle using that setting. This is nolonger the case in version 16.0.0 multi has evolved ;) **Improved:** - Performance at the base level has been doubled! On my machine benchmark went from ~9mil to ~20 mil steps/s. diff --git a/init.lua b/init.lua index 26eb411..a197fb4 100644 --- a/init.lua +++ b/init.lua @@ -1,7 +1,7 @@ --[[ MIT License -Copyright (c) 2022 Ryan Ward +Copyright (c) 2023 Ryan Ward Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -27,22 +27,73 @@ local mainloopActive = false local isRunning = false local clock = os.clock local thread = {} -local in_proc = false local processes = {} local find_optimization = false +local threadManager +local __CurrentConnectionThread -if not _G["$multi"] then - _G["$multi"] = {multi=multi,thread=thread} +multi.unpack = table.unpack or unpack +multi.pack = table.pack or function(...) return {...} end + +if table.unpack then unpack = table.unpack end + +-- Types + +multi.DestroyedObj = { + Type = "DESTROYED", +} + +local function uni() + return multi.DestroyedObj end -multi.Version = "15.3.1" +local function uniN() end +function multi.setType(obj,t) + if t == multi.DestroyedObj then + for i,v in pairs(obj) do + obj[i] = nil + end + setmetatable(obj, { + __index = function(t,k) + return setmetatable({},{__index = uni,__newindex = uni,__call = uni,__metatable = multi.DestroyedObj,__tostring = function() return "destroyed" end,__unm = uni,__add = uni,__sub = uni,__mul = uni,__div = uni,__mod = uni,__pow = uni,__concat = uni}) + end,__newindex = uni,__call = uni,__metatable = multi.DestroyedObj,__tostring = function() return "destroyed" end,__unm = uni,__add = uni,__sub = uni,__mul = uni,__div = uni,__mod = uni,__pow = uni,__concat = uni + }) + end +end + +setmetatable(multi.DestroyedObj, { + __index = function(t,k) + return setmetatable({},{__index = uni,__newindex = uni,__call = uni,__metatable = multi.DestroyedObj,__tostring = function() return "destroyed" end,__unm = uni,__add = uni,__sub = uni,__mul = uni,__div = uni,__mod = uni,__pow = uni,__concat = uni}) + end,__newindex = uni,__call = uni,__metatable = multi.DestroyedObj,__tostring = function() return "destroyed" end,__unm = uni,__add = uni,__sub = uni,__mul = uni,__div = uni,__mod = uni,__pow = uni,__concat = uni +}) + +multi.DESTROYED = multi.DestroyedObj + +-- I don't like modifying the global namespace, so I prepend a "$" +if not _G["$multi"] then + _G["$multi"] = {multi = multi, thread = thread} +end + +local types = {} +function multi.registerType(typ, p) + if multi[typ:upper():gsub("_","")] then return typ end + multi[typ:upper():gsub("_","")] = typ + table.insert(types, {typ, p or typ}) + return typ +end + +function multi.getTypes() + return types +end + +multi.Version = "16.0.0" multi.Name = "root" multi.NIL = {Type="NIL"} local NIL = multi.NIL multi.Mainloop = {} multi.Children = {} multi.Active = true -multi.Type = "rootprocess" +multi.Type = multi.registerType("rootprocess") multi.LinkedPath = multi multi.TIMEOUT = "TIMEOUT" multi.TID = 0 @@ -82,6 +133,8 @@ function multi.Stop() mainloopActive = false end +local pack = multi.pack + --Processor local priorityTable = {[false]="Disabled",[true]="Enabled"} local ProcessName = {"SubProcessor","MainProcessor"} @@ -91,6 +144,10 @@ function multi:getProcessors() return processes end +function multi:isType(type) + return self.Type == type +end + function multi:getStats() local stats = { [multi.Name] = { @@ -111,23 +168,37 @@ end --Helpers +function multi.setClock(c) + clock = c +end + function multi.ForEach(tab,func) for i=1,#tab do func(tab[i]) end end -local CRef = { - Fire = function() end -} + +function multi.randomString(n) + local str = '' + local strings = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','1','2','3','4','5','6','7','8','9','0','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'} + for i=1,n do + str = str..''..strings[math.random(1,#strings)] + end + return str +end local optimization_stats = {} local ignoreconn = true +local empty_func = function() end function multi:newConnection(protect,func,kill) + local processor = self local c={} - local call_funcs = {} local lock = false - c.callback = func - c.Parent=self + local fast = {} + c.__connectionAdded = function() end + c.rawadd = false + c.Parent = self - setmetatable(c,{__call=function(self,...) + setmetatable(c,{ + __call=function(self,...) local t = ... if type(t)=="table" then for i,v in pairs(t) do @@ -145,8 +216,76 @@ function multi:newConnection(protect,func,kill) return self:Connect(...) end end, + __unm = function(obj) -- -obj Reverses the order of connected events + local conns = obj:Bind({}) + for i = #conns, 1, -1 do + obj.rawadd = true + obj(conns[i]) + end + return obj + end, + __mod = function(obj1, obj2) -- % + local cn = self:newConnection() + if type(obj1) == "function" and type(obj2) == "table" then + obj2(function(...) + cn:Fire(obj1(...)) + end) + else + error("Invalid mod!", type(obj1), type(obj2),"Expected function, connection(table)") + end + return cn + end, + __div = function(obj1, obj2) -- / + local cn = self:newConnection() + local ref + if type(obj1) == "function" and type(obj2) == "table" then + obj2(function(...) + local args = {obj1(...)} + if args[1] then + cn:Fire(multi.unpack(args)) + end + end) + else + multi.error("Invalid divide!", type(obj1), type(obj2),"Expected function/connection(table)") + end + return cn + end, + __concat = function(obj1, obj2) -- .. + local cn = self:newConnection() + local ref + if type(obj1) == "function" and type(obj2) == "table" then + cn(function(...) + if obj1(...) then + obj2:Fire(...) + end + end) + cn.__connectionAdded = function(conn, func) + cn:Unconnect(conn) + obj2:Connect(func) + end + elseif type(obj1) == "table" and type(obj2) == "function" then + ref = cn(function(...) + obj1:Fire(...) + obj2(...) + end) + cn.__connectionAdded = function() + cn.rawadd = true + cn:Unconnect(ref) + ref = cn(function(...) + if obj2(...) then + obj1:Fire(...) + end + end) + end + elseif type(obj1) == "table" and type(obj2) == "table" then + -- + else + error("Invalid concat!", type(obj1), type(obj2),"Expected function/connection(table), connection(table)/function") + end + return cn + end, __add = function(c1,c2) -- Or - local cn = multi:newConnection() + local cn = self:newConnection() c1(function(...) cn:Fire(...) end) @@ -156,7 +295,8 @@ function multi:newConnection(protect,func,kill) return cn end, __mul = function(c1,c2) -- And - local cn = multi:newConnection() + local cn = self:newConnection() + local ref1, ref2 if c1.__hasInstances == nil then cn.__hasInstances = {2} cn.__count = {0} @@ -166,31 +306,31 @@ function multi:newConnection(protect,func,kill) cn.__count = c1.__count end - c1(function(...) + ref1 = c1(function(...) cn.__count[1] = cn.__count[1] + 1 - c1:Lock() + c1:Lock(ref1) if cn.__count[1] == cn.__hasInstances[1] then cn:Fire(...) cn.__count[1] = 0 - c1:Unlock() - c2:Unlock() + c1:Unlock(ref1) + c2:Unlock(ref2) end end) - c2(function(...) + ref2 = c2(function(...) cn.__count[1] = cn.__count[1] + 1 - c2:Lock() + c2:Lock(ref2) if cn.__count[1] == cn.__hasInstances[1] then cn:Fire(...) cn.__count[1] = 0 - c1:Unlock() - c2:Unlock() + c1:Unlock(ref1) + c2:Unlock(ref2) end end) return cn end}) - c.Type='connector' + c.Type=multi.registerType("connector", "connections") c.func={} c.ID=0 local protect=protect or false @@ -198,23 +338,34 @@ function multi:newConnection(protect,func,kill) c.FC=0 function c:hasConnections() - return #call_funcs~=0 + return #fast~=0 end - function c:getConnection(name,ignore) - if ignore then - return connections[name] or CRef - else - return connections[name] or self + function c:Lock(conn) + if conn and not conn.lock then + conn.lock = function() end + for i = 1, #fast do + if fast[conn.ref] == fast[i] then + fast[i] = conn.lock + return self + end + end + return self end - end - - function c:Lock() lock = true return self end - function c:Unlock() + function c:Unlock(conn) + if conn and conn.lock then + for i = 1, #fast do + if conn.lock == fast[i] then + fast[i] = fast[conn.ref] + return self + end + end + return self + end lock = false return self end @@ -222,110 +373,89 @@ function multi:newConnection(protect,func,kill) if protect then function c:Fire(...) if lock then return end - for i=#call_funcs,1,-1 do - if not call_funcs[i] then return end - local suc, err = pcall(call_funcs[i],...) + local kills = {} + for i=1,#fast do + local suc, err = pcall(fast[i], ...) if not suc then - print(err) + multi.error(err) end if kill then - table.remove(call_funcs,i) + table.insert(kills,i) + processor:newTask(function() + for _, k in pairs(kills) do + table.remove(kills, _) + table.remove(fast, k) + end + end) + end + end + end + end + + function c:getConnections() + return fast + end + + function c:getConnection(name, ignore) + return fast[name] or function() multi:warning("") end + end + + function c:Unconnect(conn) + for i = 1, #fast do + if fast[conn.ref] == fast[i] then + table.remove(self) + return table.remove(fast, i), i + end + end + end + + function c:fastMode() return self end + + if kill then + local kills = {} + function c:Fire(...) + if lock then return end + for i=1,#fast do + fast[i](...) + if kill then + table.insert(kills,i) + processor:newTask(function() + for _, k in pairs(kills) do + table.remove(kills, _) + table.remove(fast, k) + end + end) end end end else function c:Fire(...) - if lock then return end - for i=#call_funcs,1,-1 do - call_funcs[i](...) - if kill then - table.remove(call_funcs,i) - end - end - end - end - - local fast = {} - function c:getConnections() - return call_funcs - end - - function c:Unconnect(conn) - if conn.fast then - for i = 1, #fast do - if conn.ref == fast[i] then - table.remove(fast, i) - end - end - elseif conn.Destroy then - conn:Destroy() - end - end - - function c:fastMode() - if find_optimization then return self end - function self:Fire(...) if lock then return end for i=1,#fast do fast[i](...) end end - function self:Connect(func) - table.insert(fast, func) - local temp = {fast = true} - setmetatable(temp,{ - __call=function(s,...) - return self:Connect(...) - end, - __index = function(t,k) - if rawget(t,"root_link") then - return t["root_link"][k] - end - return nil - end, - __newindex = function(t,k,v) - if rawget(t,"root_link") then - t["root_link"][k] = v - end - rawset(t,k,v) - end, - }) - temp.ref = func - return temp + end + + function c:Connect(func, name) + local th + if thread.getRunningThread then + th = thread.getRunningThread() end - return self - end - - function c:Bind(t) - local temp = call_funcs - call_funcs=t - return temp - end - - function c:Remove() - local temp = call_funcs - call_funcs={} - return temp - end - - local function conn_helper(self,func,name,num) - self.ID=self.ID+1 - - if num then - table.insert(call_funcs,num,func) - else - table.insert(call_funcs,1,func) - end - - local temp = { - func=func, - Type="connector_link", - Parent=self, - connect = function(s,...) - return self:Connect(...) + if th then + local fref = func + func = function(...) + __CurrentConnectionThread = th + fref(...) end - } - + end + table.insert(fast, func) + if name then + fast[name] = func + else + fast["Conn_"..multi.randomString(12)] = func + end + local temp = {fast = true} setmetatable(temp,{ __call=function(s,...) return self:Connect(...) @@ -343,61 +473,33 @@ function multi:newConnection(protect,func,kill) rawset(t,k,v) end, }) - - function temp:Fire(...) - return call_funcs(...) + temp.ref = multi.randomString(24) + fast[temp.ref] = func + temp.name = name + if self.rawadd then + self.rawadd = false + else + table.insert(self,true) + self.__connectionAdded(temp, func) end - - function temp:Destroy() - multi.print("Calling Destroy on a connection link is deprecated and will be removed in v16.0.0") - for i=#call_funcs,1,-1 do - if call_funcs[i]~=nil then - if call_funcs[i]==self.func then - table.remove(call_funcs,i) - self.remove=function() end - multi.setType(temp,multi.DestroyedObj) - end - end - end - end - - if name then - connections[name]=temp - end - - if self.callback then - self.callback(temp) - end - return temp end - function c:Connect(...)--func,name,num - local tab = {...} - local funcs={} - for i=1,#tab do - if type(tab[i])=="function" then - funcs[#funcs+1] = tab[i] - end - end - if #funcs>1 then - local ret = {} - for i=1,#funcs do - table.insert(ret,conn_helper(self,funcs[i])) - end - return ret - else - return conn_helper(self,tab[1],tab[2],tab[3]) - end + function c:Bind(t) + local temp = fast + fast=t + return temp end - function c:SetHelper(func) - conn_helper = func - return self + function c:Remove() + local temp = fast + fast={} + return temp end - if find_optimization then - -- + function c:Hold() + local rets = {multi.hold(self)} + return unpack(rets) end c.connect=c.Connect @@ -405,43 +507,31 @@ function multi:newConnection(protect,func,kill) c.HasConnections = c.hasConnections c.GetConnection = c.getConnection - if not(ignoreconn) then - multi:create(c) + if func then + c = c .. func end + + if not(ignoreconn) then + if not self then return c end + self:create(c) + end + return c end -multi.enableOptimization = multi:newConnection() -multi.optConn = multi:newConnection(true) -multi.optConn(function(msg) - table.insert(optimization_stats, msg) -end) - -function multi:getOptimizationConnection() - return multi.optConn -end - -function multi:getOptimizationStats() - return optimization_stats -end - -function multi:isFindingOptimizing() - return find_optimization -end - -- Used with ISO Threads -local function isolateFunction(func,env) - local dmp = string.dump(func) - local env = env or {} - if setfenv then - local f = loadstring(dmp,"IsolatedThread_PesudoThreading") - setfenv(f,env) - return f - else - return load(dmp,"IsolatedThread_PesudoThreading","bt",env) - end +local function isolateFunction(func, env) + if setfenv then + return setfenv(func, env) + else + local env = env or {} + local dmp = string.dump(func) + return load(dmp,"IsolatedThread_PesudoThreading", "bt", env) + end end +multi.isolateFunction = isolateFunction + function multi:Break() self:Pause() self.Active=nil @@ -464,19 +554,20 @@ end function multi:SetTime(n) if not n then n=3 end local c=self:newBase() - c.Type='timemaster' + c.Type=multi.registerType("timemaster") c.timer=self:newTimer() c.timer:Start() c.set=n c.link=self - c.OnTimedOut = multi:newConnection() - c.OnTimerResolved = multi:newConnection() + c.OnTimedOut = self:newConnection() + c.OnTimerResolved = self:newConnection() self._timer=c.timer function c:Act() if self.timer:Get()>=self.set then self.link:Pause() self.OnTimedOut:Fire(self.link) self:Destroy() + return true end end return self @@ -492,24 +583,18 @@ end -- Timer stuff done multi.PausedObjects = {} function multi:Pause() - if self.Type=='rootprocess' then + if self.Type==multi.registerType("rootprocess") then multi.print("You cannot pause the main process. Doing so will stop all methods and freeze your program! However if you still want to use multi:_Pause()") else self.Active=false - local loop = self.Parent.Mainloop - for i=1,#loop do - if loop[i] == self then - multi.PausedObjects[self] = true - table.remove(loop,i) - break - end - end + self._Act = self.Act + self.Act = empty_func end return self end function multi:Resume() - if self.Type=='process' or self.Type=='rootprocess' then + if self.Type==multi.registerType("process", "processes") or self.Type==multi.registerType("rootprocess") then self.Active=true local c=self:getChildren() for i=1,#c do @@ -517,8 +602,7 @@ function multi:Resume() end else if self.Active==false then - table.insert(self.Parent.Mainloop,self) - multi.PausedObjects[self] = nil + self.Act = self._Act self.Active=true end end @@ -526,10 +610,10 @@ function multi:Resume() end function multi:Destroy() - if self.Type=='process' or self.Type=='rootprocess' then + if self.Type==multi.registerType("process", "processes") or self.Type==multi.registerType("rootprocess") then local c=self:getChildren() for i=1,#c do - self.OnObjectDestroyed:Fire(c[i]) + self.OnObjectDestroyed:Fire(c[i], self) c[i]:Destroy() end local new = {} @@ -546,7 +630,7 @@ function multi:Destroy() else for i=#self.Parent.Mainloop,1,-1 do if self.Parent.Mainloop[i]==self then - self.Parent.OnObjectDestroyed:Fire(self) + self.Parent.OnObjectDestroyed:Fire(self, self.Parent) table.remove(self.Parent.Mainloop,i) self.Destroyed = true break @@ -567,7 +651,8 @@ function multi:isDone() end function multi:create(ref) - self.OnObjectCreated:Fire(ref,self) + ref.UID = "U"..multi.randomString(12) + self.OnObjectCreated:Fire(ref, self) return self end @@ -579,9 +664,9 @@ end --Constructors [CORE] local _tid = 0 function multi:newBase(ins) - if not(self.Type=='rootprocess' or self.Type=='process') then error('Can only create an object on multi or an interface obj') return false end + if not(self.Type==multi.registerType("rootprocess") or self.Type==multi.registerType("process", "processes")) then multi.error('Can only create an object on multi or an interface obj') return false end local c = {} - if self.Type=='process' then + if self.Type==multi.registerType("process", "processes") then setmetatable(c, {__index = multi}) else setmetatable(c, {__index = multi}) @@ -590,11 +675,23 @@ function multi:newBase(ins) c.func={} c.funcTM={} c.funcTMR={} - c.OnBreak = multi:newConnection() + c.OnBreak = self:newConnection() + c.OnPriorityChanged = self:newConnection() c.TID = _tid c.Act=function() end c.Parent=self - c.creationTime = os.clock() + c.creationTime = clock() + + function c:Pause() + c.Parent.Pause(self) + return self + end + + function c:Resume() + c.Parent.Resume(self) + return self + end + if ins then table.insert(self.Mainloop,ins,c) else @@ -604,23 +701,14 @@ function multi:newBase(ins) return c end -function multi:newConnector() - local c = {Type = "connector"} - return c -end - -multi.OnObjectCreated=multi:newConnection() -multi.OnObjectDestroyed=multi:newConnection() -multi.OnLoad = multi:newConnection(nil,nil,true) -ignoreconn = false function multi:newTimer() local c={} - c.Type='timer' + c.Type=multi.registerType("timer", "timers") local time=0 local count=0 local paused=false function c:Start() - time=os.clock() + time=clock() return self end function c:Get() @@ -638,46 +726,51 @@ function multi:newTimer() end function c:Resume() paused=false - time=os.clock()-time + time=clock()-time return self end - multi:create(c) + self:create(c) return c end --Core Actors -function multi:newEvent(task) +function multi:newEvent(task, func) local c=self:newBase() - c.Type='event' + c.Type=multi.registerType("event", "events") local task = task or function() end function c:Act() local t = task(self) if t then self:Pause() self.returns = t - c.OnEvent:Fire(self) + self.OnEvent:Fire(self) + return true end end function c:SetTask(func) task=func return self end - c.OnEvent = self:newConnection():fastMode() + c.OnEvent = self:newConnection() + if func then + c.OnEvent(func) + end self:setPriority("core") - c:SetName(c.Type) - multi:create(c) + c:setName(c.Type) + self:create(c) return c end -function multi:newUpdater(skip) +function multi:newUpdater(skip, func) local c=self:newBase() - c.Type='updater' + c.Type=multi.registerType("updater", "updaters") local pos = 1 local skip = skip or 1 function c:Act() if pos >= skip then pos = 0 self.OnUpdate:Fire(self) + return true end pos = pos+1 end @@ -685,15 +778,18 @@ function multi:newUpdater(skip) skip=n return self end - c.OnUpdate = self:newConnection():fastMode() - c:SetName(c.Type) - multi:create(c) + c.OnUpdate = self:newConnection() + c:setName(c.Type) + if func then + c.OnUpdate(func) + end + self:create(c) return c end -function multi:newAlarm(set) +function multi:newAlarm(set, func) local c=self:newBase() - c.Type='alarm' + c.Type=multi.registerType("alarm", "alarms") c:setPriority("Low") c.set=set or 0 local count = 0 @@ -704,6 +800,7 @@ function multi:newAlarm(set) self.Active=false self.OnRing:Fire(self) t = clock() + return true end end function c:Resume() @@ -717,45 +814,51 @@ function multi:newAlarm(set) t = clock() return self end - c.OnRing = self:newConnection():fastMode() + c.OnRing = self:newConnection() function c:Pause() count = clock() self.Parent.Pause(self) return self end - c:SetName(c.Type) - multi:create(c) + if func then + c.OnRing(func) + end + c:setName(c.Type) + self:create(c) return c end -function multi:newLoop(func,notime) +function multi:newLoop(func, notime) local c=self:newBase() - c.Type='loop' + c.Type = multi.registerType("loop", "loops") local start=clock() if notime then function c:Act() self.OnLoop:Fire(self) + return true end else function c:Act() self.OnLoop:Fire(self,clock()-start) + return true end end - c.OnLoop = self:newConnection():fastMode() + + c.OnLoop = self:newConnection() if func then c.OnLoop(func) end - multi:create(c) - c:SetName(c.Type) + self:create(c) + c:setName(c.Type) return c end function multi:newStep(start,reset,count,skip) local c=self:newBase() think=1 - c.Type='step' + c.Type=multi.registerType("step", "steps") c.pos=start or 1 c.endAt=reset or math.huge c.skip=skip or 0 @@ -786,11 +889,12 @@ function multi:newStep(start,reset,count,skip) if self.spos>=self.skip then self.spos=0 end + return true end c.Reset=c.Resume - c.OnStart = self:newConnection():fastMode() - c.OnStep = self:newConnection():fastMode() - c.OnEnd = self:newConnection():fastMode() + c.OnStart = self:newConnection() + c.OnStep = self:newConnection() + c.OnEnd = self:newConnection() function c:Break() self.Active=nil return self @@ -806,54 +910,64 @@ function multi:newStep(start,reset,count,skip) self:Resume() return self end - c:SetName(c.Type) - multi:create(c) + c:setName(c.Type) + self:create(c) return c end -function multi:newTLoop(func,set) +function multi:newTLoop(func, set) local c=self:newBase() - c.Type='tloop' + c.Type=multi.registerType("tloop", "tloops") c.set=set or 0 c.timer=self:newTimer() c.life=0 c:setPriority("Low") + function c:Act() - if self.timer:Get()>=self.set then + if self.timer:Get() >= self.set then self.life=self.life+1 self.timer:Reset() - self.OnLoop:Fire(self,self.life) + self.OnLoop:Fire(self, self.life) + return true end end + function c:Set(set) self.set = set end + function c:Resume() self.Parent.Resume(self) self.timer:Resume() return self end + function c:Pause() self.timer:Pause() self.Parent.Pause(self) return self end - c.OnLoop = self:newConnection():fastMode() + + c.OnLoop = self:newConnection() + if func then c.OnLoop(func) end - c:SetName(c.Type) - multi:create(c) + + c:setName(c.Type) + + self:create(c) + return c end -function multi:setTimeout(func,t) - thread:newThread(function() thread.sleep(t) func() end) +function multi:setTimeout(func, t) + thread:newThread("TimeoutThread", function() thread.sleep(t) func() end) end function multi:newTStep(start,reset,count,set) local c=self:newStep(start,reset,count) - c.Type='tstep' + c.Type=multi.registerType("tstep", "tsteps") c:setPriority("Low") local reset = reset or math.huge c.timer=clock() @@ -881,6 +995,7 @@ function multi:newTStep(start,reset,count,set) self.OnEnd:Fire(self) self.pos=self.start end + return true end end function c:Set(set) @@ -892,35 +1007,15 @@ function multi:newTStep(start,reset,count,set) self:Resume() return self end - c:SetName(c.Type) - multi:create(c) + c:setName(c.Type) + self:create(c) return c end -local tasks = {} -local _tasks = 0 - -local function _task_handler() - tasks[#tasks + 1] = func - _tasks = _tasks + 1 -end +multi.tasks = {} function multi:newTask(func) - multi:newThread("Task Handler",function() - while true do - thread.hold(function() - return _tasks > 0 - end) - for i=1,_tasks do - tasks[i]() - end - _tasks = 0 - end - end) - -- Re bind this method to use the one that doesn't init a thread! - multi.newTask = _task_handler - tasks[#tasks + 1] = func - _tasks = _tasks + 1 + self.tasks[#self.tasks + 1] = func end local scheduledjobs = {} @@ -965,6 +1060,14 @@ function multi.getCurrentTask() return __CurrentTask end +function multi:setCurrentProcess() + __CurrentProcess = self +end + +function multi:setCurrentTask() + __CurrentTask = self +end + function multi:getName() return self.Name end @@ -975,25 +1078,35 @@ end local sandcount = 1 -function multi:newProcessor(name,nothread) +function multi:newProcessor(name, nothread, priority) local c = {} setmetatable(c,{__index = multi}) - local name = name or "Processor_"..sandcount + local name = name or "Processor_" .. sandcount sandcount = sandcount + 1 c.Mainloop = {} - c.Type = "process" + c.Type = multi.registerType("process", "processes") local Active = nothread or false + local task_delay = 0 c.Name = name or "" + c.tasks = {} c.threads = {} c.startme = {} c.parent = self + c.OnObjectCreated = self:newConnection() - local handler = c:createHandler(c.threads,c.startme) + local boost = 1 + local handler + + if priority then + handler = c:createPriorityHandler(c) + else + handler = c:createHandler(c) + end if not nothread then -- Don't create a loop if we are triggering this manually c.process = self:newLoop(function() if Active then - c:uManager() + c:uManager(true) handler() end end) @@ -1002,10 +1115,14 @@ function multi:newProcessor(name,nothread) c.process.PID = sandcount c.OnError = c.process.OnError else - c.OnError = multi:newConnection() + c.OnError = self:newConnection() + end + + c.OnError(multi.error) + + function c:getHandler() + return handler end - c.OnError(multi.print) - function c:getThreads() return c.threads @@ -1019,22 +1136,40 @@ function multi:newProcessor(name,nothread) return self.Name end - function c:newThread(name,func,...) - in_proc = c - local t = thread.newThread(c,name,func,...) - in_proc = false - return t + function c:newThread(name, func,...) + return thread.newThread(c, name, func, ...) end - function c:newFunction(func,holdme) + function c:newFunction(func, holdme) return thread:newFunctionBase(function(...) - return c:newThread("TempThread",func,...) - end,holdme)() + return c:newThread("Process Threaded Function Handler", func, ...) + end, holdme)() + end + + function c:boost(count) + boost = count or 1 + if boost > 1 then + self.run = function() + if not Active then return end + for i=1,boost do + c:uManager(true) + handler() + end + return c + end + else + self.run = function() + if not Active then return end + c:uManager(true) + handler() + return c + end + end end function c.run() if not Active then return end - c:uManager() + c:uManager(true) handler() return c end @@ -1057,42 +1192,54 @@ function multi:newProcessor(name,nothread) Active = false c.process:Destroy() end + + function c:setTaskDelay(delay) + if type(delay) == "function" then + task_delay = delay + else + task_delay = tonumber(delay) or 0 + end + end + + c:newThread("Task Handler", function() + local self = multi:getCurrentProcess() + local function task_holder() + return #self.tasks > 0 + end + while true do + if #self.tasks > 0 then + table.remove(self.tasks,1)() + else + thread.hold(task_holder) + end + if task_delay~=0 then + thread.hold(task_delay) + end + end + end).OnError(multi.error) table.insert(processes,c) + self:create(c) return c end function multi.hold(func,opt) - if thread.isThread() then - if type(func) == "function" or type(func) == "table" then - return thread.hold(func,opt) - end - return thread.sleep(func) - end - local death = false + if thread.isThread() then return thread.hold(func, opt) end local proc = multi.getCurrentTask() - proc:Pause() - if type(func)=="number" then - thread:newThread("Hold_func",function() - thread.hold(func) - death = true - end) - while not death do - multi:uManager() - end - proc:Resume() - else - local rets - thread:newThread("Hold_func",function() - rets = {thread.hold(func,opt)} - death = true - end) - while not death do - multi:uManager() - end - proc:Resume() - return unpack(rets) + if proc then + proc:Pause() end + local rets + thread:newThread("Hold_func",function() + rets = {thread.hold(func,opt)} + end) + while rets == nil do + multi:uManager() + end + if proc then + proc:Resume() + end + return multi.unpack(rets) end -- Threading stuff @@ -1113,7 +1260,7 @@ function multi:getThreads() return threads end -function multi:getTasks() +function multi:getRunners() local tasks = {} for i,v in pairs(self.Mainloop) do if not v.__ignore then @@ -1124,7 +1271,15 @@ function multi:getTasks() end function thread.request(t,cmd,...) - thread.requests[t.thread] = {cmd,{...}} + thread.requests[t.thread] = {cmd, multi.pack(...)} +end + +function thread.defer(func) + local th = thread.getRunningThread() + local conn = (th.OnError + th.OnDeath) + conn(function() + func(th) + end) end function thread.getRunningThread() @@ -1144,10 +1299,14 @@ function thread._Requests() if t then thread.requests[running()] = nil local cmd,args = t[1],t[2] - thread[cmd](unpack(args)) + thread[cmd](multi.unpack(args)) end end +function thread.exec(func) + func() +end + function thread.sleep(n) thread._Requests() thread.getRunningThread().lastSleep = clock() @@ -1159,12 +1318,18 @@ local function conn_test(conn) local args local func = function(...) ready = true - args = {...} + args = multi.pack(...) end - conn(func) + + local ref = conn(func) return function() if ready then - return unpack(args) or multi.NIL + conn:Unconnect(ref) + if #args==0 then + return multi.NIL + else + return multi.unpack(args) + end end end end @@ -1176,10 +1341,10 @@ function thread.chain(...) end end -function thread.hold(n,opt) +function thread.hold(n, opt) thread._Requests() local opt = opt or {} - if type(opt)=="table" then + if type(opt)=="table" and type(n) == "function" then interval = opt.interval if opt.cycles then return yield(CMD, t_holdW, opt.cycles or 1, n or dFunc, interval) @@ -1189,16 +1354,18 @@ function thread.hold(n,opt) return yield(CMD, t_skip, opt.skip or 1, nil, interval) end end - + if type(n) == "number" then thread.getRunningThread().lastSleep = clock() return yield(CMD, t_sleep, n or 0, nil, interval) - elseif type(n) == "table" and n.Type == "connector" then + elseif type(n) == "table" and n.Type == multi.registerType("connector", "connections") then return yield(CMD, t_hold, conn_test(n), nil, interval) + elseif type(n) == "table" and n.Hold ~= nil then + return n:Hold(opt) elseif type(n) == "function" then - return yield(CMD, t_hold, n or dFunc, nil, interval) + return yield(CMD, t_hold, n, nil, interval) else - error("Invalid argument passed to thread.hold(...)!") + multi.error("Invalid argument passed to thread.hold(...) ".. type(n) .. "!") end end @@ -1218,7 +1385,7 @@ function thread.skip(n) end function thread.kill() - error("thread killed!") + multi.error("thread killed!") end function thread.yield() @@ -1227,11 +1394,12 @@ function thread.yield() end function thread.isThread() - if _VERSION~="Lua 5.1" then - local a,b = running() + local a,b = running() + if b then + -- We are dealing with luajit compat or 5.2+ return not(b) else - return running()~=nil + return a~=nil end end @@ -1254,7 +1422,7 @@ function thread.waitFor(name) end local function cleanReturns(...) - local returns = {...} + local returns = multi.pack(...) local rets = {} local ind = 0 for i=#returns,1,-1 do @@ -1263,17 +1431,15 @@ local function cleanReturns(...) break end end - return unpack(returns,1,ind) + return multi.unpack(returns,1,ind) end function thread.pushStatus(...) - local t = thread.getRunningThread() + local t = thread.getRunningThread() or __CurrentConnectionThread t.statusconnector:Fire(...) end -local handler - -function thread:newFunctionBase(generator,holdme) +function thread:newFunctionBase(generator, holdme, TYPE) return function() local tfunc = {} tfunc.Active = true @@ -1296,21 +1462,25 @@ function thread:newFunctionBase(generator,holdme) if err then return multi.NIL, err elseif rets then - return cleanReturns((rets[1] or multi.NIL),rets[2],rets[3],rets[4],rets[5],rets[6],rets[7],rets[8],rets[9],rets[10],rets[11],rets[12],rets[13],rets[14],rets[15],rets[16]) + local g = rets + rets = nil + return cleanReturns((g[1] or multi.NIL),g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15],g[16]) end end) else while not rets and not err do - handler() + multi:uManager() end + local g = rets + rets = nil if err then return nil,err end - return cleanReturns(rets[1],rets[2],rets[3],rets[4],rets[5],rets[6],rets[7],rets[8],rets[9],rets[10],rets[11],rets[12],rets[13],rets[14],rets[15],rets[16]) + return cleanReturns(g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15],g[16]) end end - tfunc.__call = function(t,...) - if t.Active == false then + tfunc.__call = function(th,...) + if th.Active == false then if holdme then return nil, "Function is paused" end @@ -1321,79 +1491,155 @@ function thread:newFunctionBase(generator,holdme) f(nil,"Function is paused") end } - end + end local t = generator(...) - t.OnDeath(function(...) rets = {...} end) + t.OnDeath(function(...) rets = multi.pack(...) end) t.OnError(function(self,e) err = e end) if holdme then return wait() end local temp = { - OnStatus = multi:newConnection(true), - OnError = multi:newConnection(true), - OnReturn = multi:newConnection(true), + OnStatus = multi:getCurrentProcess():newConnection(true), + OnError = multi:getCurrentProcess():newConnection(true), + OnReturn = multi:getCurrentProcess():newConnection(true), isTFunc = true, wait = wait, getReturns = function() - return unpack(rets) + return multi.unpack(rets) end, connect = function(f) - local tempConn = multi:newConnection(true) + local tempConn = multi:getCurrentProcess():newConnection(true) t.OnDeath(function(...) if f then f(...) else tempConn:Fire(...) end end) t.OnError(function(self,err) if f then f(nil,err) else tempConn:Fire(nil,err) end end) return tempConn end } t.OnDeath(function(...) temp.OnReturn:Fire(...) end) - t.OnError(function(self,err) temp.OnError:Fire(err) end) + t.OnError(function(self,err) temp.OnError:Fire(err) temp.OnError(multi.error) end) t.linkedFunction = temp t.statusconnector = temp.OnStatus return temp end - setmetatable(tfunc,tfunc) + setmetatable(tfunc, tfunc) + tfunc.Type = TYPE or multi.registerType("function", "functions") return tfunc end end -function thread:newFunction(func,holdme) +function thread:newFunction(func, holdme) return thread:newFunctionBase(function(...) - return thread:newThread("TempThread",func,...) - end,holdme)() + return thread:newThread("Free Threaded Function Handler", func, ...) + end, holdme)() +end + +function thread:newProcessor(name, nothread, priority) + -- Inactive proxy proc + local process = multi:getCurrentProcess() + local proc = process:newProcessor(name, true) + local thread_proc = process:newProcessor(name).Start() + local Active = true + + local handler + if priority then + handler = thread_proc:createPriorityHandler(c) + else + handler = thread_proc:createHandler(c) + end + + function proc:getThreads() + return thread_proc.threads + end + + function proc:getFullName() + return thread_proc.parent:getFullName() .. "." .. self.Name + end + + function proc:getName() + return thread_proc.Name + end + + function proc:isActive() + return Active + end + + function proc:newThread(name, func, ...) + return thread.newThread(thread_proc, name, func, ...) + end + + function proc:newFunction(func, holdme) + return thread:newFunctionBase(function(...) + return thread_proc:newThread("TProc Threaded Function Handler", func, ...) + end, holdme)() + end + + function proc.Start() + Active = true + return proc + end + + function proc.Stop() + Active = false + return proc + end + + function proc:Destroy() + Active = false + thread_proc:Destroy() + end + + proc.OnObjectCreated(function(obj) + if not obj.Act then return end + multi.print("Converting "..obj.Type.." to thread!") + thread_proc:newThread(function() + obj.reallocate = empty_func + while true do + thread.hold(function() return Active end) + obj:Act() + end + end) + end) + + process:create(proc) + + return proc end -- A cross version way to set enviroments, not the same as fenv though function multi.setEnv(func,env) local f = string.dump(func) - local chunk = load(f,"env","bt",env) + local chunk = load(f, "env", "bt", env) return chunk end -local threads = {} -local startme = {} -local startme_len = 0 -function thread:newThread(name,func,...) +function thread:newThread(name, func, ...) multi.OnLoad:Fire() -- This was done incase a threaded function was called before mainloop/uManager was called - local func = func or name - if func == name then - name = name or multi.randomString(16) + if type(name) == "function" then + func = name + name = "UnnamedThread_"..multi.randomString(16) end local c={nil,nil,nil,nil,nil,nil,nil} - local env = {self=c} c.TempRets = {nil,nil,nil,nil,nil,nil,nil,nil,nil,nil} - c.startArgs = {...} + c.startArgs = multi.pack(...) c.ref={} c.Name=name c.thread=create(func) c.sleep=1 - c.Type="thread" + c.Type = multi.registerType("thread", "threads") c.TID = threadid c.firstRunDone=false c._isPaused = false c.returns = {} - c.isError = false - c.OnError = multi:newConnection(true,nil,true) - c.OnDeath = multi:newConnection(true,nil,true) - c.OnError(multi.print) + c.isError = false + + if self.Type == multi.registerType("process", "processes") then + c.OnError = self:newConnection(true,nil,true) + c.OnDeath = self:newConnection(true,nil,true) + else + c.OnError = threadManager:newConnection(true,nil,true) + c.OnDeath = threadManager:newConnection(true,nil,true) + end + + c.OnError(multi.error) function c:getName() return c.Name @@ -1406,7 +1652,7 @@ function thread:newThread(name,func,...) local resumed = false function c:Pause() if not self._isPaused then - thread.request(self,"exec",function() + thread.request(self, "exec", function() thread.hold(function() return resumed end) @@ -1424,12 +1670,12 @@ function thread:newThread(name,func,...) end function c:Kill() - thread.request(self,"kill") + thread.request(self, "kill") return self end function c:Sleep(n) - thread.request(self,"exec",function() + thread.request(self, "exec", function() thread.sleep(n) resumed = false end) @@ -1437,32 +1683,42 @@ function thread:newThread(name,func,...) end function c:Hold(n,opt) - thread.request(self,"exec",function() - thread.hold(n,opt) + thread.request(self, "exec", function() + thread.hold(n, opt) resumed = false end) return self end c.Destroy = c.Kill - - if self.Type=="process" then - table.insert(self.startme,c) + if thread.isThread() then + if self.Type == multi.registerType("process", "processes") then + table.insert(self.startme, c) + else + table.insert(threadManager.startme, c) + end else - table.insert(startme,c) + if self.Type == multi.registerType("process", "processes") then + table.insert(self.startme, c) + else + table.insert(threadManager.startme, c) + end end - - startme_len = #startme + globalThreads[c] = multi threadid = threadid + 1 - multi:create(c) - c.creationTime = os.clock() + if self.Type == multi.registerType("process", "processes") then + self:create(c) + else + threadManager:create(c) + end + c.creationTime = clock() return c end -function thread:newISOThread(name,func,_env,...) +function thread:newISOThread(name, func, env, ...) local func = func or name - local env = _env or {} + local env = env or {} if not env.thread then env.thread = thread end @@ -1472,8 +1728,8 @@ function thread:newISOThread(name,func,_env,...) if type(name) == "function" then name = "Thread#"..threadCount end - local func = isolateFunction(func,env) - return thread:newThread(name,func,...) + local func = isolateFunction(func, env) + return thread:newThread(name, func, ...) end multi.newThread = thread.newThread @@ -1605,14 +1861,15 @@ co_status = { end r1=nil r2=nil r3=nil r4=nil r5=nil end, - ["normal"] = function(thd,ref) end, - ["running"] = function(thd,ref) end, + ["normal"] = function(thd,ref) end, + ["running"] = function(thd,ref) end, ["dead"] = function(thd,ref,task,i,th) - if ref.__processed then return end + if ref.__processed then table.remove(th,i) return end if _ then ref.OnDeath:Fire(ret,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,r13,r14,r15,r16) else ref.OnError:Fire(ref,ret,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,r13,r14,r15,r16) + multi.error(ref, ret) end if i then table.remove(th,i) @@ -1624,45 +1881,21 @@ co_status = { end end end - _=nil r1=nil r2=nil r3=nil r4=nil r5=nil + _=nil r1=nil r2=nil r3=nil r4=nil r5=nil r6=nil r7=nil r8=nil r9=nil r10=nil r11=nil r12=nil r13=nil r14=nil r15=nil r16=nil ref.__processed = true end, } -handler = coroutine.wrap(function(self) - local temp_start - while true do - for start = #startme, 1, -1 do - temp_start = startme[start] - table.remove(startme) - _,ret,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,r13,r14,r15,r16 = resume(temp_start.thread,unpack(temp_start.startArgs)) - co_status[status(temp_start.thread)](temp_start.thread,temp_start,t_none,nil,threads) -- Make sure there was no error - table.insert(threads,temp_start) - yield() - end - for i=#threads,1,-1 do - ref = threads[i] - if ref then - task = ref.task - thd = ref.thread - ready = ref.__ready - co_status[status(thd)](thd,ref,task,i,threads) - end - yield() - end - yield() - end -end) -function multi:createHandler(threads,startme) - return coroutine.wrap(function(self) +function multi:createHandler() + local threads, startme = self.threads, self.startme + return coroutine.wrap(function() local temp_start while true do - for start = #startme, 1, -1 do - temp_start = startme[start] - table.remove(startme) - _,ret,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,r13,r14,r15,r16 = resume(temp_start.thread,unpack(temp_start.startArgs)) - co_status[status(temp_start.thread)](temp_start.thread,temp_start,t_none,nil,threads) -- Make sure there was no error - table.insert(threads,temp_start) + while #startme>0 do + temp_start = table.remove(startme) + _, ret, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16 = resume(temp_start.thread, multi.unpack(temp_start.startArgs)) + co_status[status(temp_start.thread)](temp_start.thread, temp_start, t_none, nil, threads) + table.insert(threads, temp_start) yield() end for i=#threads,1,-1 do @@ -1671,7 +1904,7 @@ function multi:createHandler(threads,startme) task = ref.task thd = ref.thread ready = ref.__ready - co_status[status(thd)](thd,ref,task,i,threads) + co_status[status(thd)](thd, ref, task, i, threads) end yield() end @@ -1680,9 +1913,34 @@ function multi:createHandler(threads,startme) end) end +function multi:createPriorityHandler() + local threads, startme = self.threads, self.startme + return coroutine.wrap(function() + local temp_start + while true do + while #startme>0 do + temp_start = table.remove(startme) + _, ret, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16 = resume(temp_start.thread, multi.unpack(temp_start.startArgs)) + co_status[status(temp_start.thread)](temp_start.thread, temp_start, t_none, nil, threads) + table.insert(threads, temp_start) + end + for i=#threads,1,-1 do + ref = threads[i] + if ref then + task = ref.task + thd = ref.thread + ready = ref.__ready + co_status[status(thd)](thd, ref, task, i, threads) + end + end + yield() + end + end) +end + function multi:newService(func) -- Priority managed threads local c = {} - c.Type = "service" + c.Type = multi.registerType("service", "services") c.OnStopped = self:newConnection() c.OnStarted = self:newConnection() local Service_Data = {} @@ -1698,7 +1956,7 @@ function multi:newService(func) -- Priority managed threads time = self:newTimer() time:Start() active = true - c:OnStarted(c,Service_Data) + c:OnStarted(c, Service_Data) end return c end @@ -1707,18 +1965,19 @@ function multi:newService(func) -- Priority managed threads thread.hold(function() return active end) - func(c,Service_Data) + func(c, Service_Data) task(ap) return c end - local th = thread:newThread(function() + local th = thread:newThread("Service_Handler",function() while true do process() end end) th.OnError = c.OnError -- use the threads onerror as our own + th.OnError(multi.error) function c.Destroy() th:kill() @@ -1782,13 +2041,13 @@ function multi:newService(func) -- Priority managed threads return c end - multi.create(multi,c) + self:create(c) return c end -- Multi runners -local function mainloop(self) +function multi:mainloopRef() __CurrentProcess = self multi.OnPreLoad:Fire() self.uManager = self.uManagerRef @@ -1802,19 +2061,18 @@ local function mainloop(self) for _D=#Loop,1,-1 do __CurrentTask = Loop[_D] ctask = __CurrentTask - ctask:Act() + if ctask then ctask:Act() end __CurrentProcess = self end - handler() end else return nil, "Already Running!" end end -multi.mainloop = mainloop +multi.mainloop = multi.mainloopRef -local function p_mainloop(self) +function multi:p_mainloop() __CurrentProcess = self multi.OnPreLoad:Fire() self.uManager = self.uManagerRefP1 @@ -1835,30 +2093,96 @@ local function p_mainloop(self) end end end - handler() end else return nil, "Already Running!" end end -local init = false -function multi.init(settings, realsettings) - if settings == multi then settings = realsettings end - if init then return _G["$multi"].multi,_G["$multi"].thread end - init = true - if type(settings)=="table" then - multi.defaultSettings = settings - if settings.priority then - multi.mainloop = p_mainloop - else - multi.mainloop = mainloop +local function doOpt() + function thread.hold(n,opt) + thread._Requests() + local opt = opt or {} + if type(opt)=="table" then + interval = opt.interval + if opt.cycles then + return yield(CMD, t_holdW, opt.cycles or 1, n or dFunc, interval) + elseif opt.sleep then + return yield(CMD, t_holdF, opt.sleep, n or dFunc, interval) + elseif opt.skip then + return yield(CMD, t_skip, opt.skip or 1, nil, interval) + end end - if settings.findopt then - find_optimization = true - multi.enableOptimization:Fire(multi, thread) + if type(n) == "number" then + thread.getRunningThread().lastSleep = clock() + return yield(CMD, t_sleep, n or 0, nil, interval) + elseif type(n) == "table" and n.Type == multi.registerType("connector", "connections") then + local rdy = function() + return false + end + n(function(a1,a2,a3,a4,a5,a6) + rdy = function() + if a1==nil then + return NIL,a2,a3,a4,a5,a6 + end + return a1,a2,a3,a4,a5,a6 + end + end) + return yield(CMD, t_hold, function() + return rdy() + end, nil, interval) + elseif type(n) == "function" then + local cache = string.dump(n) + local f_str = tostring(n) + local good = true + for i=1,#func_cache do + if func_cache[i][1] == cache and func_cache[i][2] ~= f_str and not func_cache[i][3] then + multi:getOptimizationConnection():Fire("It's better to store a function to a variable than to use an anonymous function within the hold method!\n" .. debug.traceback()) + func_cache[i][3] = true + good = false + end + end + if good then + table.insert(func_cache, {cache, f_str}) + end + return yield(CMD, t_hold, n or dFunc, nil, interval) + else + multi.error("Invalid argument passed to thread.hold(...)!") end end +end + +local init = false +multi.settingsHook = multi:newConnection() +function multi.init(settings, realsettings) + if settings == multi then settings = realsettings end + + if type(settings)=="table" then + + multi.defaultSettings = settings + + if settings.priority then + multi.mainloop = multi.p_mainloop + else + multi.mainloop = multi.mainloopRef + end + + if not init then + + if settings.findopt then + find_optimization = true + doOpt() + multi.enableOptimization:Fire(multi, thread) + end + + if settings.debugging then + require("multi.integration.debugManager") + end + + multi.settingsHook:Fire(settings) + end + end + init = true return _G["$multi"].multi,_G["$multi"].thread end @@ -1868,7 +2192,6 @@ function multi:uManager() multi.OnPreLoad:Fire() self.uManager=self.uManagerRef multi.OnLoad:Fire() - handler() end end @@ -1885,7 +2208,6 @@ function multi:uManagerRefP1() end end end - handler() end end @@ -1898,7 +2220,6 @@ function multi:uManagerRef() __CurrentTask:Act() __CurrentProcess = self end - handler() end end @@ -1921,43 +2242,13 @@ function table.merge(t1, t2) return t1 end -if table.unpack and not unpack then - unpack=table.unpack -end - -multi.DestroyedObj = { - Type = "destroyed", -} - -local function uni() - return multi.DestroyedObj -end - -local function uniN() end -function multi.setType(obj,t) - if t == multi.DestroyedObj then - for i,v in pairs(obj) do - obj[i] = nil - end - setmetatable(obj, { - __index = function(t,k) - return setmetatable({},{__index = uni,__newindex = uni,__call = uni,__metatable = multi.DestroyedObj,__tostring = function() return "destroyed" end,__unm = uni,__add = uni,__sub = uni,__mul = uni,__div = uni,__mod = uni,__pow = uni,__concat = uni}) - end,__newindex = uni,__call = uni,__metatable = multi.DestroyedObj,__tostring = function() return "destroyed" end,__unm = uni,__add = uni,__sub = uni,__mul = uni,__div = uni,__mod = uni,__pow = uni,__concat = uni - }) - end -end -setmetatable(multi.DestroyedObj, { - __index = function(t,k) - return setmetatable({},{__index = uni,__newindex = uni,__call = uni,__metatable = multi.DestroyedObj,__tostring = function() return "destroyed" end,__unm = uni,__add = uni,__sub = uni,__mul = uni,__div = uni,__mod = uni,__pow = uni,__concat = uni}) - end,__newindex = uni,__call = uni,__metatable = multi.DestroyedObj,__tostring = function() return "destroyed" end,__unm = uni,__add = uni,__sub = uni,__mul = uni,__div = uni,__mod = uni,__pow = uni,__concat = uni -}) math.randomseed(os.time()) function multi:enableLoadDetection() if multi.maxSpd then return end -- here we are going to run a quick benchMark solo local temp = self:newProcessor() - local t = os.clock() + local t = clock() local stop = false temp:benchMark(.01):OnBench(function(time,steps) stop = steps @@ -1998,6 +2289,7 @@ function multi:getLoad() end function multi:setPriority(s) + if not self:IsAnActor() or self.Type == multi.registerType("process", "processes") then return end if type(s)=="number" then self.Priority=s elseif type(s)=='string' then @@ -2020,6 +2312,7 @@ function multi:setPriority(s) elseif s:lower()=='idle' or s:lower()=='i' then self.Priority=self.Priority_Idle end + self.OnPriorityChanged:Fire(self, self.Priority) end if not self.PrioritySet then self.defPriority = self.Priority @@ -2051,15 +2344,6 @@ else end end -function multi.randomString(n) - local str = '' - local strings = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','1','2','3','4','5','6','7','8','9','0','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'} - for i=1,n do - str = str..''..strings[math.random(1,#strings)] - end - return str -end - function multi:getChildren() return self.Mainloop end @@ -2095,7 +2379,7 @@ function multi:benchMark(sec,p,pt) c=c+1 end end) - temp.OnBench = multi:newConnection() + temp.OnBench = self:newConnection() temp:setPriority(p or 1) return temp end @@ -2148,12 +2432,15 @@ function multi:IsAnActor() return self.Act~=nil end -function multi:reallocate(o,n) - n=n or #o.Mainloop+1 +function multi:reallocate(processor, index) + index=index or #processor.Mainloop+1 local int=self.Parent - self:Destroy() - self.Parent=o - table.insert(o.Mainloop,n,self) + self.Parent=processor + if index then + table.insert(processor.Mainloop, index, self) + else + table.insert(processor.Mainloop, self) + end self.Active=true return self end @@ -2164,7 +2451,7 @@ function multi.timer(func,...) args={func(...)} local t = timer:Get() timer = nil - return t,unpack(args) + return t,multi.unpack(args) end if os.getOS()=="windows" then @@ -2175,16 +2462,61 @@ end function multi.print(...) if multi.defaultSettings.print then - print(...) + local t = {} + for i,v in ipairs(multi.pack(...)) do t[#t+1] = tostring(v) end + io.write("\x1b[94mINFO:\x1b[0m " .. table.concat(t," ") .. "\n") end end +function multi.warn(...) + if multi.defaultSettings.warn then + local t = {} + for i,v in ipairs(multi.pack(...)) do t[#t+1] = tostring(v) end + io.write("\x1b[93mWARNING:\x1b[0m " .. table.concat(t," ") .. "\n") + end +end + +function multi.debug(...) + if multi.defaultSettings.debugging then + local t = {} + for i,v in ipairs(multi.pack(...)) do t[#t+1] = tostring(v) end + io.write("\x1b[97mDEBUG:\x1b[0m " .. table.concat(t," ") + .. "\n" .. multi:getCurrentProcess():getFullName() + .. " " .. (multi:getCurrentTask() and multi:getCurrentTask().Type or "Unknown Type") .. "\n" .. + ((coroutine.running()) and debug.traceback((coroutine.running())) or debug.traceback()) .. "\n") + end +end + +function multi.error(self, err) + if type(err) == "bool" then crash = err end + if type(self) == "string" then err = self end + local name = debug.getinfo(2).name + if name then + io.write("\x1b[91mERROR:\x1b[0m " .. err .. " " .. name .."\n") + else + io.write("\x1b[91mERROR:\x1b[0m " .. err .. " ?\n") + end + if multi.defaultSettings.error then + error("^^^ " .. multi:getCurrentProcess():getFullName() .. " " .. multi:getCurrentTask().Type .. "\n" .. + ((coroutine.running()) and debug.traceback((coroutine.running())) or debug.traceback()) .. "\n") + os.exit(1) + end +end + +function multi.success(...) + local t = {} + for i,v in ipairs(multi.pack(...)) do t[#t+1] = tostring(v) end + io.write("\x1b[92mSUCCESS:\x1b[0m " .. table.concat(t," ") .. "\n") +end + +-- Old things for compatability multi.GetType = multi.getType multi.IsPaused = multi.isPaused multi.IsActive = multi.isActive -multi.Reallocate = multi.Reallocate +multi.Reallocate = multi.reallocate multi.ConnectFinal = multi.connectFinal multi.ResetTime = multi.SetTime +multi.setTime = multi.SetTime multi.IsDone = multi.isDone multi.SetName = multi.setName @@ -2196,10 +2528,33 @@ function os.exit(n) _os(n) end +multi.OnObjectCreated=multi:newConnection() +ignoreconn = false +multi.OnObjectDestroyed=multi:newConnection() +multi.OnLoad = multi:newConnection(nil,nil,true) + +multi.enableOptimization = multi:newConnection() +multi.optConn = multi:newConnection(true) +multi.optConn(function(msg) + table.insert(optimization_stats, msg) +end) + +function multi:getOptimizationConnection() + return multi.optConn +end + +function multi:getOptimizationStats() + return optimization_stats +end + +function multi:isFindingOptimizing() + return find_optimization +end + multi.OnError=multi:newConnection() multi.OnPreLoad = multi:newConnection() multi.OnExit = multi:newConnection(nil,nil,true) -multi.m = {onexit = function() multi.OnExit:Fire() end} +multi.m = {onexit = function() os.exit() end} if _VERSION >= "Lua 5.2" or jit then setmetatable(multi.m, {__gc = multi.m.onexit}) @@ -2207,64 +2562,20 @@ else multi.m.sentinel = newproxy(true) getmetatable(multi.m.sentinel).__gc = multi.m.onexit end -local func_cache = {} -multi:newThread(function() - thread.skip() - if find_optimization then - - function thread.hold(n,opt) - thread._Requests() - local opt = opt or {} - if type(opt)=="table" then - interval = opt.interval - if opt.cycles then - return yield(CMD, t_holdW, opt.cycles or 1, n or dFunc, interval) - elseif opt.sleep then - return yield(CMD, t_holdF, opt.sleep, n or dFunc, interval) - elseif opt.skip then - return yield(CMD, t_skip, opt.skip or 1, nil, interval) - end - end - - if type(n) == "number" then - thread.getRunningThread().lastSleep = clock() - return yield(CMD, t_sleep, n or 0, nil, interval) - elseif type(n) == "table" and n.Type == "connector" then - local rdy = function() - return false - end - n(function(a1,a2,a3,a4,a5,a6) - rdy = function() - if a1==nil then - return NIL,a2,a3,a4,a5,a6 - end - return a1,a2,a3,a4,a5,a6 - end - end) - return yield(CMD, t_hold, function() - return rdy() - end, nil, interval) - elseif type(n) == "function" then - local cache = string.dump(n) - local f_str = tostring(n) - local good = true - for i=1,#func_cache do - if func_cache[i][1] == cache and func_cache[i][2] ~= f_str and not func_cache[i][3] then - multi:getOptimizationConnection():Fire("It's better to store a function to a variable than to use an anonymous function within the hold method!\n" .. debug.traceback()) - func_cache[i][3] = true - good = false - end - end - if good then - table.insert(func_cache, {cache, f_str}) - end - return yield(CMD, t_hold, n or dFunc, nil, interval) - else - error("Invalid argument passed to thread.hold(...)!") - end - end - -- Add more Overrides - end -end) + +threadManager = multi:newProcessor("Global_Thread_Manager", nil, true).Start() +threadManager.tasks = multi.tasks -- The main multi interface is a bit different. + +function multi:setTaskDelay(delay) + threadManager:setTaskDelay(delay) +end + +function multi:getThreadManagerProcess() + return threadManager +end + +function multi:getHandler() + return threadManager:getHandler() +end return multi \ No newline at end of file diff --git a/integration/debugManager/init.lua b/integration/debugManager/init.lua new file mode 100644 index 0000000..c467547 --- /dev/null +++ b/integration/debugManager/init.lua @@ -0,0 +1,106 @@ +local multi, thread = require("multi"):init() + +multi.defaultSettings.debugging = true + +local dbg = {} +dbg.__index = dbg +dbg.processors = {} + +-- Hooks to all on object created events! +local c_cache = {} +local d_cache = {} + +local proc = multi:newProcessor("Debug_Processor").Start() + +dbg.OnObjectCreated = function(obj, process) + if c_cache[obj] then + return false + else + c_cache[obj] = true + proc:newTask(function() + c_cache[obj] = false + end) + return true + end +end .. multi:newConnection() + +dbg.OnObjectDestroyed = function(obj, process) + if d_cache[obj] then + return false + else + d_cache[obj] = true + proc:newTask(function() + d_cache[obj] = false + end) + return true + end +end .. multi:newConnection() + +local creation_hook, destruction_hook +local types +local objects = {} + +creation_hook = function(obj, process) + types = multi:getTypes() + if obj.Type == multi.PROCESS and not dbg.processors[obj] then + obj.OnObjectCreated(creation_hook) + obj.OnObjectDestroyed(destruction_hook) + end + + table.insert(objects, obj) + + dbg.OnObjectCreated:Fire(obj, process) +end + +destruction_hook = function(obj, process) + for i = 1, #objects do + if objects[i] == obj then + table.remove(objects, i) + break + end + end + dbg.OnObjectDestroyed:Fire(obj, process) +end + +function dbg:getObjects(typ) + if type(typ) == "string" then + local objs = {} + for i = 1, #objects do + if objects[i].Type == typ then + objs[#objs+1] = objects[i] + end + end + return objs + elseif type(typ) == "table" then -- Process + local objs = {} + for i = 1, #objects do + if objects[i].Parent == typ then + objs[#objs+1] = objects[i] + end + end + return objs + elseif type(typ) == "function" then + local objs = {} + -- Keep objects local/private, return true to add to list, false to reject, "break" to break loop + for i = 1, #objects do + local ret = typ(objects[i]) + if ret then + objs[#objs+1] = objects[i] + elseif ret == "break" then + break + end + end + return objs + end +end + +local debug_stats = {} + +local tmulti = multi:getThreadManagerProcess() +multi.OnObjectCreated(creation_hook) +tmulti.OnObjectCreated(creation_hook) +multi.OnObjectDestroyed(destroction_hook) +tmulti.OnObjectDestroyed(destroction_hook) + +-- We write to a debug interface in the multi namespace +multi.debugging = dbg diff --git a/integration/effilManager/extensions.lua b/integration/effilManager/extensions.lua new file mode 100644 index 0000000..e69de29 diff --git a/integration/effilManager/init.lua b/integration/effilManager/init.lua new file mode 100644 index 0000000..f5c1277 --- /dev/null +++ b/integration/effilManager/init.lua @@ -0,0 +1,46 @@ +local multi, thread = require("multi"):init{error=true} +multi.error("Currntly not supported!") +os.exit(1) +local effil = require("effil") + +-- I like some of the things that this library offers. +-- Current limitations prevent me from being able to use effil, +-- but I might fork and work on it myself. + +-- Configs +effil.allow_table_upvalues(false) + +local GLOBAL,THREAD = require("multi.integration.effilManager.threads").init() +local count = 1 +local started = false +local livingThreads = {} + +function multi:newSystemThread(name, func, ...) + local name = name or multi.randomString(16) + local rand = math.random(1, 10000000) + c = {} + c.name = name + c.Name = name + c.Id = count +end + +function THREAD:newFunction(func, holdme) + return thread:newFunctionBase(function(...) + return multi:newSystemThread("TempSystemThread",func,...) + end, holdme, multi.SFUNCTION)() +end + +THREAD.newSystemThread = function(...) + multi:newSystemThread(...) +end + +multi.print("Integrated Effil Threading!") +multi.integration = {} -- for module creators +multi.integration.GLOBAL = GLOBAL +multi.integration.THREAD = THREAD +require("multi.integration.effilManager.extensions") +return { + init = function() + return GLOBAL, THREAD + end +} \ No newline at end of file diff --git a/integration/effilManager/threads.lua b/integration/effilManager/threads.lua new file mode 100644 index 0000000..e69de29 diff --git a/integration/lanesManager/extensions.lua b/integration/lanesManager/extensions.lua index 9600dc6..618762f 100644 --- a/integration/lanesManager/extensions.lua +++ b/integration/lanesManager/extensions.lua @@ -22,29 +22,57 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] local multi, thread = require("multi"):init() + if not (GLOBAL and THREAD) then - local GLOBAL, THREAD = multi.integration.GLOBAL,multi.integration.THREAD + GLOBAL, THREAD = multi.integration.GLOBAL, multi.integration.THREAD else lanes = require("lanes") end + function multi:newSystemThreadedQueue(name) local name = name or multi.randomString(16) local c = {} c.Name = name c.linda = lanes.linda() + c.Type = multi.registerType("s_queue") + function c:push(v) self.linda:send("Q", v) end + function c:pop() return ({self.linda:receive(0, "Q")})[2] end + function c:peek() return self.linda:get("Q") end + function c:init() return self end - GLOBAL[name or "_"] = c + + if multi.isMainThread then + multi.integration.GLOBAL[name] = c + else + GLOBAL[name] = c + end + + function c:Hold(opt) + local multi, thread = require("multi"):init() + if opt.peek then + return thread.hold(function() + return self:peek() + end) + else + return thread.hold(function() + return self:pop() + end) + end + end + + self:create(c) + return c end @@ -53,37 +81,60 @@ function multi:newSystemThreadedTable(name) local c = {} c.link = lanes.linda() c.Name = name - setmetatable(c,{ + c.Type = multi.registerType("s_table") + + function c:init() + return self + end + + setmetatable(c,{ __index = function(t,k) return c.link:get(k) end, __newindex = function(t,k,v) - c.link:set(k,v) + c.link:set(k, v) end }) - function c:init() - return self + + if multi.isMainThread then + multi.integration.GLOBAL[name] = c + else + GLOBAL[name] = c + end + + function c:Hold(opt) + local multi, thread = require("multi"):init() + if opt.key then + return thread.hold(function() + return self.tab[opt.key] + end) + else + multi.error("Must provide a key to check opt.key = 'key'") + end end - GLOBAL[name or "_"] = c + + self:create(c) + return c end function multi:newSystemThreadedJobQueue(n) local c = {} c.cores = n or THREAD.getCores()*2 + c.Type = multi.registerType("s_jobqueue") c.OnJobCompleted = multi:newConnection() - local funcs = multi:newSystemThreadedTable():init() - local queueJob = multi:newSystemThreadedQueue():init() - local queueReturn = multi:newSystemThreadedQueue():init() - local doAll = multi:newSystemThreadedQueue():init() + local funcs = multi:newSystemThreadedTable() + local queueJob = multi:newSystemThreadedQueue() + local queueReturn = multi:newSystemThreadedQueue() + local doAll = multi:newSystemThreadedQueue() local ID=1 local jid = 1 function c:isEmpty() return queueJob:peek()==nil end - function c:doToAll(func) + function c:doToAll(func,...) for i=1,c.cores do - doAll:push{ID,func} + doAll:push{ID,func,...} end ID = ID + 1 return self @@ -93,12 +144,12 @@ function multi:newSystemThreadedJobQueue(n) return self end function c:pushJob(name,...) - queueJob:push{name,jid,{...}} + queueJob:push{name,jid,multi.pack(...)} jid = jid + 1 return jid-1 end local nFunc = 0 - function c:newFunction(name,func,holup) -- This registers with the queue + function c:newFunction(name, func, holup) -- This registers with the queue if type(name)=="function" then holup = func func = name @@ -112,32 +163,38 @@ function multi:newSystemThreadedJobQueue(n) local rets link = c.OnJobCompleted(function(jid,...) if id==jid then - rets = {...} - link:Destroy() + rets = multi.pack(...) end end) return thread.hold(function() if rets then - return unpack(rets) or multi.NIL + if #rets == 0 then + return multi.NIL + else + return multi.unpack(rets) + end end end) - end,holup),name + end, holup), name end thread:newThread("JobQueueManager",function() while true do local job = thread.hold(function() return queueReturn:pop() end) - local id = table.remove(job,1) - c.OnJobCompleted:Fire(id,unpack(job)) + if job then + local id = table.remove(job,1) + c.OnJobCompleted:Fire(id,multi.unpack(job)) + end end end) for i=1,c.cores do - multi:newSystemThread("SystemThreadedJobQueue",function(queue) - local multi,thread = require("multi"):init() + multi:newSystemThread("STJQ_"..multi.randomString(8),function(queue) + local multi, thread = require("multi"):init() local idle = os.clock() local clock = os.clock local ref = 0 + _G["__QR"] = queueReturn setmetatable(_G,{__index = funcs}) thread:newThread("JobHandler",function() while true do @@ -145,10 +202,12 @@ function multi:newSystemThreadedJobQueue(n) return queueJob:pop() end) idle = clock() - local name = table.remove(dat,1) - local jid = table.remove(dat,1) - local args = table.remove(dat,1) - queueReturn:push{jid, funcs[name](unpack(args)),queue} + thread:newThread("JobQueue-Spawn",function() + local name = table.remove(dat, 1) + local jid = table.remove(dat, 1) + local args = table.remove(dat, 1) + queueReturn:push{jid, funcs[name](args[1],args[2],args[3],args[4],args[5],args[6],args[7],args[8]), queue} + end) end end) thread:newThread("DoAllHandler",function() @@ -158,9 +217,10 @@ function multi:newSystemThreadedJobQueue(n) end) if dat then if dat[1]>ref then + ref = table.remove(dat, 1) + func = table.remove(dat, 1) idle = clock() - ref = dat[1] - dat[2]() + func(unpack(dat)) doAll:pop() end end @@ -175,14 +235,22 @@ function multi:newSystemThreadedJobQueue(n) end end) multi:mainloop() - end,i).priority = thread.Priority_Core + end,i) end + + function c:Hold(opt) + return thread.hold(self.OnJobCompleted) + end + + self:create(c) + return c end function multi:newSystemThreadedConnection(name) local name = name or multi.randomString(16) local c = {} + c.Type = multi.registerType("s_connection") c.CONN = 0x00 c.TRIG = 0x01 c.PING = 0x02 @@ -196,7 +264,7 @@ function multi:newSystemThreadedConnection(name) end return r end - c.CID = THREAD.getID() + c.CID = THREAD_ID c.subscribe = multi:newSystemThreadedQueue("SUB_STC_"..self.Name):init() c.Name = name c.links = {} -- All triggers sent from main connection. When a connection is triggered on another thread, they speak to the main then send stuff out. @@ -233,7 +301,7 @@ function multi:newSystemThreadedConnection(name) local function fire(...) for _, link in pairs(c.links) do - link:push {c.TRIG, {...}} + link:push {c.TRIG, multi.pack(...)} end end @@ -253,16 +321,16 @@ function multi:newSystemThreadedConnection(name) end) c.links[#c.links+1] = item[2] elseif item[1] == c.TRIG then - fire(unpack(item[2])) - c.proxy_conn:Fire(unpack(item[2])) + fire(multi.unpack(item[2])) + c.proxy_conn:Fire(multi.unpack(item[2])) end end end) --- ^^^ This will only exist in the init thread function c:Fire(...) - local args = {...} - if self.CID == THREAD.getID() then -- Host Call + local args = multi.pack(...) + if self.CID == THREAD_ID then -- Host Call for _, link in pairs(self.links) do link:push {self.TRIG, args} end @@ -277,8 +345,14 @@ function multi:newSystemThreadedConnection(name) self.links = {} self.proxy_conn = multi:newConnection() local mt = getmetatable(self.proxy_conn) - setmetatable(self, {__index = self.proxy_conn, __call = function(t,func) self.proxy_conn(func) end, __add = mt.__add}) - if self.CID == THREAD.getID() then return self end + local tempMT = {} + for i,v in pairs(mt) do + tempMT[i] = v + end + tempMT.__index = self.proxy_conn + tempMT.__call = function(t,func) self.proxy_conn(func) end + setmetatable(self, tempMT) + if self.CID == THREAD_ID then return self end thread:newThread("STC_CONN_MAN"..name,function() local item local link_self_ref = multi:newSystemThreadedQueue() @@ -296,7 +370,7 @@ function multi:newSystemThreadedConnection(name) end link_self_ref:pop() elseif item[1] == self.TRIG then - self.proxy_conn:Fire(unpack(item[2])) + self.proxy_conn:Fire(multi.unpack(item[2])) link_self_ref:pop() else -- This shouldn't be the case @@ -306,7 +380,14 @@ function multi:newSystemThreadedConnection(name) return self end - GLOBAL[name] = c + if multi.isMainThread then + multi.integration.GLOBAL[name] = c + else + GLOBAL[name] = c + end + + self:create(c) return c -end \ No newline at end of file +end +require("multi.integration.sharedExtensions") \ No newline at end of file diff --git a/integration/lanesManager/init.lua b/integration/lanesManager/init.lua index d53e12f..f9d0848 100644 --- a/integration/lanesManager/init.lua +++ b/integration/lanesManager/init.lua @@ -36,6 +36,9 @@ lanes = require("lanes").configure() multi.SystemThreads = {} multi.isMainThread = true +_G.THREAD_NAME = "MAIN_THREAD" +_G.THREAD_ID = 0 + function multi:canSystemThread() return true end @@ -55,10 +58,10 @@ local count = 1 local started = false local livingThreads = {} -function THREAD:newFunction(func,holdme) +function THREAD:newFunction(func, holdme) return thread:newFunctionBase(function(...) return multi:newSystemThread("TempSystemThread",func,...) - end,holdme)() + end, holdme, multi.registerType("s_function"))() end function multi:newSystemThread(name, func, ...) @@ -67,35 +70,43 @@ function multi:newSystemThread(name, func, ...) local rand = math.random(1, 10000000) local return_linda = lanes.linda() c = {} - c.name = name c.Name = name - c.Id = count + c.ID = count c.loadString = {"base","package","os","io","math","table","string","coroutine"} livingThreads[count] = {true, name} c.returns = return_linda - c.Type = "sthread" + c.Type = multi.registerType("s_thread") c.creationTime = os.clock() c.alive = true c.priority = THREAD.Priority_Normal local multi_settings = multi.defaultSettings - for i,v in pairs(multi_settings) do - print(i,v) + local globe = { + THREAD_NAME = name, + THREAD_ID = count, + THREAD = THREAD, + GLOBAL = GLOBAL, + _Console = __ConsoleLinda, + _DEFER = {} + } + if GLOBAL["__env"] then + for i,v in pairs(GLOBAL["__env"]) do + globe[i] = v + end end c.thread = lanes.gen("*", { - globals={ -- Set up some globals - THREAD_NAME = name, - THREAD_ID = count, - THREAD = THREAD, - GLOBAL = GLOBAL, - _Console = __ConsoleLinda - }, - priority=c.priority + globals = globe, + priority = c.priority },function(...) - require("multi"):init(multi_settings) + multi, thread = require("multi"):init(multi_settings) require("multi.integration.lanesManager.extensions") + require("multi.integration.sharedExtensions") local has_error = true - return_linda:set("returns",{func(...)}) + returns = {pcall(func, ...)} + return_linda:set("returns", returns) + for i,v in pairs(_DEFER) do + pcall(v) + end has_error = false end)(...) count = count + 1 @@ -110,10 +121,20 @@ function multi:newSystemThread(name, func, ...) c.OnDeath = multi:newConnection() c.OnError = multi:newConnection() GLOBAL["__THREADS__"] = livingThreads + c.OnError(multi.error) + + if self.isActor then + self:create(c) + else + multi.create(multi, c) + end + return c end -THREAD.newSystemThread = multi.newSystemThread +THREAD.newSystemThread = function(...) + multi:newSystemThread(...) +end function multi.InitSystemThreadErrorHandler() if started == true then @@ -126,18 +147,26 @@ function multi.InitSystemThreadErrorHandler() while true do thread.yield() _,data = __ConsoleLinda:receive(0, "Q") - if data then print(unpack(data)) end + if data then + --print(data[1]) + end for i = #threads, 1, -1 do temp = threads[i] status = temp.thread.status - push = __StatusLinda:get(temp.Id) + push = __StatusLinda:get(temp.ID) if push then - temp.statusconnector:Fire(unpack(({__StatusLinda:receive(nil, temp.Id)})[2])) + temp.statusconnector:Fire(multi.unpack(({__StatusLinda:receive(nil, temp.ID)})[2])) end if status == "done" or temp.returns:get("returns") then - livingThreads[temp.Id] = {false, temp.Name} + returns = ({temp.returns:receive(0, "returns")})[2] + livingThreads[temp.ID] = {false, temp.Name} temp.alive = false - temp.OnDeath:Fire(unpack(({temp.returns:receive(0, "returns")})[2])) + if returns[1] == false then + temp.OnError:Fire(temp, returns[2]) + else + table.remove(returns,1) + temp.OnDeath:Fire(multi.unpack(returns)) + end GLOBAL["__THREADS__"] = livingThreads table.remove(threads, i) elseif status == "running" then @@ -145,19 +174,15 @@ function multi.InitSystemThreadErrorHandler() elseif status == "waiting" then -- elseif status == "error" then - livingThreads[temp.Id] = {false, temp.Name} - temp.alive = false - temp.OnError:Fire(temp,unpack(temp.returns:receive(0,"returns") or {"Thread Killed!"})) - GLOBAL["__THREADS__"] = livingThreads - table.remove(threads, i) + -- The thread never really errors, we handle this through our linda object elseif status == "cancelled" then - livingThreads[temp.Id] = {false, temp.Name} + livingThreads[temp.ID] = {false, temp.Name} temp.alive = false temp.OnError:Fire(temp,"thread_cancelled") GLOBAL["__THREADS__"] = livingThreads table.remove(threads, i) elseif status == "killed" then - livingThreads[temp.Id] = {false, temp.Name} + livingThreads[temp.ID] = {false, temp.Name} temp.alive = false temp.OnError:Fire(temp,"thread_killed") GLOBAL["__THREADS__"] = livingThreads @@ -165,10 +190,10 @@ function multi.InitSystemThreadErrorHandler() end end end - end) + end).OnError(multi.error) end -multi.print("Integrated Lanes!") +multi.print("Integrated Lanes Threading!") multi.integration = {} -- for module creators multi.integration.GLOBAL = GLOBAL multi.integration.THREAD = THREAD diff --git a/integration/lanesManager/threads.lua b/integration/lanesManager/threads.lua index f1b8da1..42433df 100644 --- a/integration/lanesManager/threads.lua +++ b/integration/lanesManager/threads.lua @@ -48,20 +48,12 @@ local function INIT(__GlobalLinda, __SleepingLinda, __StatusLinda, __Console) end function THREAD.waitFor(name) - local function wait() + local multi, thread = require("multi"):init() + return multi.hold(function() math.randomseed(os.time()) __SleepingLinda:receive(.001, "__non_existing_variable") - end - repeat - wait() - until __GlobalLinda:get(name) - return __GlobalLinda:get(name) - end - - if getOS() == "windows" then - THREAD.__CORES = tonumber(os.getenv("NUMBER_OF_PROCESSORS")) - else - THREAD.__CORES = tonumber(io.popen("nproc --all"):read("*n")) + return __GlobalLinda:get(name) + end) end function THREAD.getCores() @@ -72,11 +64,11 @@ local function INIT(__GlobalLinda, __SleepingLinda, __StatusLinda, __Console) local c = {} c.queue = __Console function c.print(...) - c.queue:send("Q", {...}) + c.queue:push("Q", table.concat(multi.pack(...), "\t")) end function c.error(err) - c.queue:push("Q",{"ERROR in <"..__THREADNAME__..">: "..err,__THREADID__}) - error(err) + c.queue:push("Q", "Error in <"..THREAD_NAME..":" .. THREAD_ID .. ">: ".. err) + multi.error(err) end return c end @@ -95,16 +87,12 @@ local function INIT(__GlobalLinda, __SleepingLinda, __StatusLinda, __Console) error("Thread was killed!\1") end - function THREAD.getName() - return THREAD_NAME - end - - function THREAD.getID() - return THREAD_ID + function THREAD.sync() + -- Maybe do something... end function THREAD.pushStatus(...) - local args = {...} + local args = multi.pack(...) __StatusLinda:send(nil,THREAD_ID, args) end @@ -134,9 +122,30 @@ local function INIT(__GlobalLinda, __SleepingLinda, __StatusLinda, __Console) __GlobalLinda:set(k, v) end }) + + function THREAD.setENV(env, name) + GLOBAL[name or "__env"] = env + end + + function THREAD.getENV(name) + return GLOBAL[name or "__env"] + end + + function THREAD.exposeENV(name) + name = name or "__env" + local env = THREAD.getENV(name) + for i,v in pairs(env) do + _G[i] = v + end + end + + function THREAD.defer(func) + table.insert(_DEFER, func) + end + return GLOBAL, THREAD end -return {init = function(g,s,st,c) - return INIT(g,s,st,c) +return {init = function(g,s,st,c,onexit) + return INIT(g,s,st,c,onexit) end} \ No newline at end of file diff --git a/integration/loveManager/extensions.lua b/integration/loveManager/extensions.lua index da5b560..ee29baf 100644 --- a/integration/loveManager/extensions.lua +++ b/integration/loveManager/extensions.lua @@ -1,106 +1,131 @@ ---[[ -MIT License - -Copyright (c) 2022 Ryan Ward - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sub-license, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -]] - if not ISTHREAD then multi, thread = require("multi").init() GLOBAL = multi.integration.GLOBAL THREAD = multi.integration.THREAD -else - GLOBAL = multi.integration.GLOBAL - THREAD = multi.integration.THREAD end function multi:newSystemThreadedQueue(name) local name = name or multi.randomString(16) + local c = {} + c.Name = name - local fRef = {"func",nil} - function c:init() - local q = {} - q.chan = love.thread.getChannel(self.Name) - function q:push(dat) - if type(dat) == "function" then - fRef[2] = THREAD.dump(dat) - self.chan:push(fRef) - return - else - self.chan:push(dat) - end - end - function q:pop() - local dat = self.chan:pop() - if type(dat)=="table" and dat[1]=="func" then - return THREAD.loadDump(dat[2]) - else - return dat - end - end - function q:peek() - local dat = self.chan:peek() - if type(dat)=="table" and dat[1]=="func" then - return THREAD.loadDump(dat[2]) - else - return dat - end - end - return q + c.Type = multi.registerType("s_queue") + c.chan = love.thread.getChannel(name) + + function c:push(dat) + self.chan:push(THREAD.packValue(dat)) + end + + function c:pop() + return THREAD.unpackValue(self.chan:pop()) + end + + function c:peek() + return THREAD.unpackValue(self.chan:peek()) + end + + function c:init() + self.chan = love.thread.getChannel(self.Name) + return self + end + + function c:Hold(opt) + local multi, thread = require("multi"):init() + if opt.peek then + return thread.hold(function() + return self:peek() + end) + else + return thread.hold(function() + return self:pop() + end) + end end - THREAD.package(name,c) + + GLOBAL[name] = c + + self:create(c) + return c end + function multi:newSystemThreadedTable(name) local name = name or multi.randomString(16) - local c = {} + + local c = {} + c.Name = name + c.Type = multi.registerType("s_table") + c.tab = THREAD.createTable(name) + function c:init() - return THREAD.createTable(self.Name) + self.tab = THREAD.createTable(self.Name) + setmetatable(self,{ + __index = function(t, k) + return self.tab[k] + end, + __newindex = function(t,k,v) + self.tab[k] = v + end + }) + return self end - THREAD.package(name,c) + + c.__init = c.init + + function c:Hold(opt) + local multi, thread = require("multi"):init() + if opt.key then + return thread.hold(function() + return self.tab[opt.key] + end) + else + multi.error("Must provide a key to check opt.key = 'key'") + end + end + + setmetatable(c,{ + __index = function(t, k) + return c.tab[k] + end, + __newindex = function(t,k,v) + c.tab[k] = v + end + }) + + GLOBAL[name] = c + + self:create(c) + return c end + local jqc = 1 function multi:newSystemThreadedJobQueue(n) local c = {} + c.cores = n or THREAD.getCores() c.registerQueue = {} - c.funcs = THREAD.createStaticTable("__JobQueue_"..jqc.."_table") - c.queue = love.thread.getChannel("__JobQueue_"..jqc.."_queue") - c.queueReturn = love.thread.getChannel("__JobQueue_"..jqc.."_queueReturn") - c.queueAll = love.thread.getChannel("__JobQueue_"..jqc.."_queueAll") + c.Type = multi.registerType("s_jobqueue") + c.funcs = THREAD.createTable("__JobQueue_"..jqc.."_table") + c.queue = multi:newSystemThreadedQueue("__JobQueue_"..jqc.."_queue") + c.queueReturn = multi:newSystemThreadedQueue("__JobQueue_"..jqc.."_queueReturn") + c.queueAll = multi:newSystemThreadedQueue("__JobQueue_"..jqc.."_queueAll") c.id = 0 c.OnJobCompleted = multi:newConnection() + local allfunc = 0 + function c:doToAll(func) - local f = THREAD.dump(func) for i = 1, self.cores do - self.queueAll:push({allfunc,f}) + self.queueAll:push({allfunc, func}) end allfunc = allfunc + 1 end - function c:registerFunction(name,func) + function c:registerFunction(name, func) if self.funcs[name] then - error("A function by the name "..name.." has already been registered!") + multi.error("A function by the name "..name.." has already been registered!") end self.funcs[name] = func end @@ -127,13 +152,12 @@ function multi:newSystemThreadedJobQueue(n) local rets link = c.OnJobCompleted(function(jid,...) if id==jid then - rets = {...} - link:Destroy() + rets = multi.pack(...) end end) return thread.hold(function() if rets then - return unpack(rets) or multi.NIL + return multi.unpack(rets) or multi.NIL end end) end,holup),name @@ -143,7 +167,7 @@ function multi:newSystemThreadedJobQueue(n) thread.yield() local dat = c.queueReturn:pop() if dat then - c.OnJobCompleted:Fire(unpack(dat)) + c.OnJobCompleted:Fire(multi.unpack(dat)) end end end) @@ -151,16 +175,15 @@ function multi:newSystemThreadedJobQueue(n) multi:newSystemThread("JobQueue_"..jqc.."_worker_"..i,function(jqc) local multi, thread = require("multi"):init() require("love.timer") - local function atomic(channel) - return channel:pop() - end + love.timer.sleep(1) local clock = os.clock - local funcs = THREAD.createStaticTable("__JobQueue_"..jqc.."_table") - local queue = love.thread.getChannel("__JobQueue_"..jqc.."_queue") - local queueReturn = love.thread.getChannel("__JobQueue_"..jqc.."_queueReturn") + local funcs = THREAD.createTable("__JobQueue_"..jqc.."_table") + local queue = THREAD.waitFor("__JobQueue_"..jqc.."_queue") + local queueReturn = THREAD.waitFor("__JobQueue_"..jqc.."_queueReturn") local lastProc = clock() - local queueAll = love.thread.getChannel("__JobQueue_"..jqc.."_queueAll") + local queueAll = THREAD.waitFor("__JobQueue_"..jqc.."_queueAll") local registry = {} + _G["__QR"] = queueReturn setmetatable(_G,{__index = funcs}) thread:newThread("startUp",function() while true do @@ -168,7 +191,7 @@ function multi:newSystemThreadedJobQueue(n) local all = queueAll:peek() if all and not registry[all[1]] then lastProc = os.clock() - THREAD.loadDump(queueAll:pop()[2])() + queueAll:pop()[2]() end end end) @@ -179,20 +202,21 @@ function multi:newSystemThreadedJobQueue(n) local all = queueAll:peek() if all and not registry[all[1]] then lastProc = os.clock() - THREAD.loadDump(queueAll:pop()[2])() + queueAll:pop()[2]() end - local dat = queue:performAtomic(atomic) + local dat = thread.hold(queue) if dat then - lastProc = os.clock() - local name = table.remove(dat,1) - local id = table.remove(dat,1) - local tab = {funcs[name](unpack(dat))} - table.insert(tab,1,id) - queueReturn:push(tab) + multi:newThread("Test",function() + lastProc = os.clock() + local name = table.remove(dat,1) + local id = table.remove(dat,1) + local tab = {funcs[name](multi.unpack(dat))} + table.insert(tab,1,id) + --local test = queueReturn.push + queueReturn:push(tab) + end) end end - end):OnError(function(...) - error(...) end) thread:newThread("Idler",function() while true do @@ -207,145 +231,14 @@ function multi:newSystemThreadedJobQueue(n) multi:mainloop() end,jqc) end + + function c:Hold(opt) + return thread.hold(self.OnJobCompleted) + end + jqc = jqc + 1 - return c -end -function multi:newSystemThreadedConnection(name) - local name = name or multi.randomString(16) - local c = {} - c.CONN = 0x00 - c.TRIG = 0x01 - c.PING = 0x02 - c.PONG = 0x03 - - local subscribe = love.thread.getChannel("SUB_STC_" .. name) - - function c:init() - - self.subscribe = love.thread.getChannel("SUB_STC_" .. self.Name) - - function self:Fire(...) - local args = {...} - if self.CID == THREAD.getID() then -- Host Call - for _, link in pairs(self.links) do - love.thread.getChannel(link):push{self.TRIG, args} - end - self.proxy_conn:Fire(...) - else - self.subscribe:push{self.TRIG, args} - end - end - - local multi, thread = require("multi"):init() - self.links = {} - self.proxy_conn = multi:newConnection() - local mt = getmetatable(self.proxy_conn) - setmetatable(self, {__index = self.proxy_conn, __call = function(t,func) self.proxy_conn(func) end, __add = mt.__add}) - if self.CID == THREAD.getID() then return self end - thread:newThread("STC_CONN_MAN" .. self.Name,function() - local item - local string_self_ref = "LSF_" .. multi.randomString(16) - local link_self_ref = love.thread.getChannel(string_self_ref) - self.subscribe:push{self.CONN, string_self_ref} - while true do - item = thread.hold(function() - return link_self_ref:peek() - end) - if item[1] == self.PING then - link_self_ref:push{self.PONG} - link_self_ref:pop() - elseif item[1] == self.CONN then - if string_self_ref ~= item[2] then - table.insert(self.links, love.thread.getChannel(item[2])) - end - link_self_ref:pop() - elseif item[1] == self.TRIG then - self.proxy_conn:Fire(unpack(item[2])) - link_self_ref:pop() - else - -- This shouldn't be the case - end - end - end).OnError(print) - return self - end - - local function remove(a, b) - local ai = {} - local r = {} - for k,v in pairs(a) do ai[v]=true end - for k,v in pairs(b) do - if ai[v]==nil then table.insert(r,a[k]) end - end - return r - end - c.CID = THREAD.getID() - c.Name = name - c.links = {} -- All triggers sent from main connection. When a connection is triggered on another thread, they speak to the main then send stuff out. - -- Locals will only live in the thread that creates the original object - local ping - local pong = function(link, links) - local res = thread.hold(function() - return love.thread.getChannel(link):peek()[1] == c.PONG - end,{sleep=3}) - - if not res then - for i=1,#links do - if links[i] == link then - table.remove(links,i,link) - break - end - end - else - love.thread.getChannel(link):pop() - end - end - - ping = thread:newFunction(function(self) - ping:Pause() - - multi.ForEach(self.links, function(link) -- Sync new connections - love.thread.getChannel(link):push{self.PING} - multi:newThread("pong Thread", pong, link, self.links) - end) - - thread.sleep(3) - - ping:Resume() - end,false) - - local function fire(...) - for _, link in pairs(c.links) do - love.thread.getChannel(link):push {c.TRIG, {...}} - end - end - - thread:newThread("STC_SUB_MAN"..name,function() - local item - while true do - thread.yield() - -- We need to check on broken connections - ping(c) -- Should return instantlly and process this in another thread - item = thread.hold(function() -- This will keep things held up until there is something to process - return c.subscribe:pop() - end) - if item[1] == c.CONN then - - multi.ForEach(c.links, function(link) -- Sync new connections - love.thread.getChannel(item[2]):push{c.CONN, link} - end) - c.links[#c.links+1] = item[2] - - elseif item[1] == c.TRIG then - fire(unpack(item[2])) - c.proxy_conn:Fire(unpack(item[2])) - end - end - end).OnError(print) - --- ^^^ This will only exist in the init thread - - THREAD.package(name,c) + self:create(c) return c end \ No newline at end of file diff --git a/integration/loveManager/init.lua b/integration/loveManager/init.lua index dd0e929..44ec815 100644 --- a/integration/loveManager/init.lua +++ b/integration/loveManager/init.lua @@ -1,122 +1,137 @@ ---[[ -MIT License - -Copyright (c) 2022 Ryan Ward - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sub-license, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -]] if ISTHREAD then error("You cannot require the loveManager from within a thread!") end + local ThreadFileData = [[ ISTHREAD = true -THREAD = require("multi.integration.loveManager.threads") -sThread = THREAD -__IMPORTS = {...} -__FUNC__=table.remove(__IMPORTS,1) -__THREADID__=table.remove(__IMPORTS,1) -__THREADNAME__=table.remove(__IMPORTS,1) -math.randomseed(__THREADID__) +args = {...} +THREAD_ID = args[1] +THREAD_NAME = args[2] +GLOBAL, THREAD, DEFER = require("multi.integration.loveManager.threads"):init() +__FUNC = THREAD.unpackValue(args[3]) +ARGS = THREAD.unpackValue(args[4]) +settings = args[5] +if ARGS == nil then ARGS = {} end +math.randomseed(THREAD_ID) math.random() math.random() math.random() -stab = THREAD.createStaticTable(__THREADNAME__) -GLOBAL = THREAD.getGlobal() -multi, thread = require("multi").init() -multi.integration={} -multi.integration.GLOBAL = GLOBAL -multi.integration.THREAD = THREAD -pcall(require,"multi.integration.loveManager.extensions") -stab["returns"] = {THREAD.loadDump(__FUNC__)(unpack(__IMPORTS))} +stab = THREAD.createTable(THREAD_NAME .. THREAD_ID) +if GLOBAL["__env"] then + local env = THREAD.getENV() + for i,v in pairs(env) do + _G[i] = v + end +end +multi, thread = require("multi"):init{error=true, warning=true, print=true, priority=true} +multi.defaultSettings.print = true +require("multi.integration.loveManager.extensions") +require("multi.integration.sharedExtensions") +local returns = {pcall(__FUNC, multi.unpack(ARGS))} +table.remove(returns,1) +stab["returns"] = returns +for i,v in pairs(DEFER) do + pcall(v) +end ]] -local multi, thread = require("multi"):init() -local THREAD = {} -__THREADID__ = 0 -__THREADNAME__ = "MainThread" -multi.integration={} -local THREAD = require("multi.integration.loveManager.threads") -local GLOBAL = THREAD.getGlobal() -local THREAD_ID = 1 -local OBJECT_ID = 0 -local stf = 0 -function multi:newSystemThread(name,func,...) +_G.THREAD_NAME = "MAIN_THREAD" +_G.THREAD_ID = 0 + +local multi, thread = require("multi"):init() +local GLOBAL, THREAD = require("multi.integration.loveManager.threads"):init() + +multi.registerType("s_function") +multi.registerType("s_thread") + +multi.integration = {} +multi.isMainThread = true +local threads = {} +local tid = 0 +function multi:newSystemThread(name, func, ...) + multi.InitSystemThreadErrorHandler() + local name = name or multi.randomString(16) + tid = tid + 1 local c = {} - c.name = name - c.ID=THREAD_ID - c.thread=love.thread.newThread(ThreadFileData) - c.thread:start(THREAD.dump(func),c.ID,c.name,...) - c.stab = THREAD.createStaticTable(name) - c.OnDeath = multi:newConnection() - c.OnError = multi:newConnection() - GLOBAL["__THREAD_"..c.ID] = {ID=c.ID, Name=c.name, Thread=c.thread} - GLOBAL["__THREAD_COUNT"] = THREAD_ID - THREAD_ID=THREAD_ID + 1 - function c:getName() - return c.name + c.Type = multi.STHREAD + c.Name = name + c.ID = tid + c.thread = love.thread.newThread(ThreadFileData) + c.thread:start(c.ID, c.Name, THREAD.packValue(func), THREAD.packValue({...}), multi.defaultSettings) + c.stab = THREAD.createTable(name .. c.ID) + c.creationTime = os.clock() + c.OnDeath = multi:newConnection() + c.OnError = multi:newConnection() + c.status_channel = love.thread.getChannel("__status_channel__" .. c.ID) + + function c:getName() return c.name end + + table.insert(threads, c) + + c.OnError(multi.error) + + if self.isActor then + self:create(c) + else + multi.create(multi, c) end - thread:newThread(function() - if name:find("TempSystemThread") then - local status_channel = love.thread.getChannel("__"..c.ID.."__MULTI__STATUS_CHANNEL__") - thread.hold(function() - -- While the thread is running we might as well do something in the loop - local status = status_channel - if status:peek()~=nil then - c.statusconnector:Fire(unpack(status:pop())) - end - return not c.thread:isRunning() - end) - else - thread.hold(function() - return not c.thread:isRunning() - end) - end - -- If the thread is not running let's handle that. - local thread_err = c.thread:getError() - if thread_err == "Thread Killed!\1" then - c.OnDeath:Fire("Thread Killed!") - elseif thread_err then - c.OnError:Fire(c,thread_err) - elseif c.stab.returns then - c.OnDeath:Fire(unpack(c.stab.returns)) - c.stab.returns = nil - end - end) + return c end -function THREAD:newFunction(func) - return thread:newFunctionBase(function(...) - return multi:newSystemThread("TempSystemThread"..THREAD_ID,func,...) - end)() +local started = false +local console_channel = love.thread.getChannel("__console_channel__") + +function THREAD:newFunction(func, holdme) + return thread:newFunctionBase(function(...) + return multi:newSystemThread("SystemThreaded Function Handler", func, ...) + end, holdme, multi.SFUNCTION)() end -THREAD.newSystemThread = multi.newSystemThread - function love.threaderror(thread, errorstr) - multi.print("Thread error!\n"..errorstr) + multi.error("Thread error! " .. errorstr) +end + +function multi.InitSystemThreadErrorHandler() + if started == true then return end + started = true + thread:newThread("Love System Thread Handler", function() + while true do + thread.yield() + for i = #threads, 1, -1 do + local th = threads[i] + if th.status_channel:peek() ~= nil then + th.statusconnector:Fire(multi.unpack(th.status_channel:pop())) + end + local th_err = th.thread:getError() + if th_err == "Thread Killed!\1" then + th.OnDeath:Fire("Thread Killed!") + table.remove(threads, i) + elseif th_err then + th.OnError:Fire(th, th_err) + table.remove(threads, i) + elseif th.stab.returns then + th.OnDeath:Fire(multi.unpack(th.stab.returns)) + th.stab.returns = nil + table.remove(threads, i) + end + end + end + end) +end + +THREAD.newSystemThread = function(...) + multi:newSystemThread(...) end multi.integration.GLOBAL = GLOBAL multi.integration.THREAD = THREAD require("multi.integration.loveManager.extensions") +require("multi.integration.sharedExtensions") multi.print("Integrated Love Threading!") -return {init=function() - return GLOBAL,THREAD -end} \ No newline at end of file + +return { + init = function() + return GLOBAL, THREAD + end +} diff --git a/integration/loveManager/threads.lua b/integration/loveManager/threads.lua index d867d6d..4ad7474 100644 --- a/integration/loveManager/threads.lua +++ b/integration/loveManager/threads.lua @@ -25,208 +25,96 @@ require("love.timer") require("love.system") require("love.data") require("love.thread") -local socket = require("socket") -local multi, thread = require("multi").init() -local threads = {} +local multi, thread = require("multi"):init() -function threads.loadDump(d) - return loadstring(d:getString()) -end - -function threads.dump(func) - return love.data.newByteData(string.dump(func)) -end - -local fRef = {"func",nil} -local function manage(channel, value) - channel:clear() - if type(value) == "function" then - fRef[2] = THREAD.dump(value) - channel:push(fRef) - return - else - channel:push(value) +-- Checks if the given value is a LOVE2D object (i.e. has metatable with __index field) and if that __index field contains functions typical of LOVE2D objects +function isLoveObject(value) + -- Check if the value has metatable + if type(value) == "userdata" and getmetatable(value) then + -- Check if the metatable has the __index field + local index = getmetatable(value).__index + if type(index) == "table" then + -- Check if the metatable's __index table contains functions typical of LOVE2D objects + if index.draw or index.update or index.getWidth or index.getHeight or index.getString or index.getPointer then + return true + end + end end + return false end -local function RandomVariable(length) - local res = {} - math.randomseed(socket.gettime()*10000) - for i = 1, length do - res[#res+1] = string.char(math.random(97, 122)) - end - return table.concat(res) -end - -local GNAME = "__GLOBAL_" -local proxy = {} -function threads.set(name,val) - if not proxy[name] then proxy[name] = love.thread.getChannel(GNAME..name) end - proxy[name]:performAtomic(manage, val) -end - -function threads.get(name) - if not proxy[name] then proxy[name] = love.thread.getChannel(GNAME..name) end - local dat = proxy[name]:peek() - if type(dat)=="table" and dat[1]=="func" then - return THREAD.loadDump(dat[2]) - else - return dat +-- Converts any function values in a table to a string with the value "\1\2:func:" where is the Lua stringified version of the function +function tableToFunctionString(t) + if type(t) == "nil" then return "\1\2:nil:" end + if type(t) == "function" then return "\1\2:func:"..string.dump(t) end + if type(t) ~= "table" then return t end + local newtable = {} + for k, v in pairs(t) do + if type(v) == "function" then + newtable[k] = "\1\2:func:"..string.dump(v) + elseif type(v) == "table" then + newtable[k] = tableToFunctionString(v) + elseif isLoveObject(v) then + newtable[k] = v + elseif type(v) == "userdata" then + newtable[k] = tostring(v) + else + newtable[k] = v + end end + return newtable end -function threads.waitFor(name) - if thread.isThread() then - return thread.hold(function() - return threads.get(name) - end) +-- Converts strings with the value "\1\2:func:" back to functions +function functionStringToTable(t) + if type(t) == "string" and t:sub(1, 8) == "\1\2:func:" then return loadstring(t:sub(9, -1)) end + if type(t) == "string" and t:sub(1, 7) == "\1\2:nil:" then return nil end + if type(t) ~= "table" then return t end + for k, v in pairs(t) do + if type(v) == "string" then + if v:sub(1, 8) == "\1\2:func:" then + t[k] = loadstring(v:sub(9, -1)) + else + t[k] = v + end + elseif type(v) == "table" then + t[k] = functionStringToTable(v) + else + t[k] = v + end end - while threads.get(name)==nil do - love.timer.sleep(.001) - end - local dat = threads.get(name) - if type(dat) == "table" and dat.init then - dat.init = threads.loadDump(dat.init) - end - return dat -end - -function threads.package(name,val) - local init = val.init - val.init=threads.dump(val.init) - GLOBAL[name]=val - val.init=init -end - -function threads.getCores() - return love.system.getProcessorCount() -end - -function threads.kill() - error("Thread Killed!\1") -end - -function threads.pushStatus(...) - local status_channel = love.thread.getChannel("__"..__THREADID__.."__MULTI__STATUS_CHANNEL__") - local args = {...} - status_channel:push(__THREADID__, args) -end - -function threads.getThreads() - local t = {} - for i=1,GLOBAL["__THREAD_COUNT"] do - t[#t+1]=GLOBAL["__THREAD_"..i] + if t.init then + t:init() end return t end -function threads.getThread(n) - return GLOBAL["__THREAD_"..n] +local function packValue(t) + return tableToFunctionString(t) end -function threads.getName() - return __THREADNAME__ +local function unpackValue(t) + return functionStringToTable(t) end -function threads.getID() - return __THREADID__ -end - -function threads.sleep(n) - love.timer.sleep(n) -end - -function threads.getGlobal() - return setmetatable({}, - { - __index = function(t, k) - return THREAD.get(k) - end, - __newindex = function(t, k, v) - THREAD.set(k,v) - end - } - ) -end - -function threads.createTable(n) - local _proxy = {} - local function set(name,val) - if not _proxy[name] then _proxy[name] = love.thread.getChannel(n..name) end - _proxy[name]:performAtomic(manage, val) +local function createTable(n) + if not n then + n = "STAB"..multi.randomString(8) end - local function get(name) - if not _proxy[name] then _proxy[name] = love.thread.getChannel(n..name) end - local dat = _proxy[name]:peek() - if type(dat)=="table" and dat[1]=="func" then - return THREAD.loadDump(dat[2]) - else - return dat - end - end - return setmetatable({}, - { - __index = function(t, k) - return get(k) - end, - __newindex = function(t, k, v) - set(k,v) - end - } - ) -end - -function threads.getConsole() - local c = {} - c.queue = love.thread.getChannel("__CONSOLE__") - function c.print(...) - c.queue:push{...} - end - function c.error(err) - c.queue:push{"ERROR in <"..__THREADNAME__..">: "..err,__THREADID__} - error(err) - end - return c -end - -if not ISTHREAD then - local clock = os.clock - local lastproc = clock() - local queue = love.thread.getChannel("__CONSOLE__") - thread:newThread("consoleManager",function() - while true do - thread.yield() - dat = queue:pop() - if dat then - lastproc = clock() - print(unpack(dat)) - end - if clock()-lastproc>2 then - thread.sleep(.1) - end - end - end) -end - -function threads.createStaticTable(n) local __proxy = {} - local function set(name,val) - if __proxy[name] then return end - local chan = love.thread.getChannel(n..name) - if chan:getCount()>0 then return end - chan:performAtomic(manage, val) - __proxy[name] = val + local function set(name, val) + local chan = love.thread.getChannel(n .. name) + if chan:getCount() == 1 then chan:pop() end + __proxy[name] = true + chan:push(packValue(val)) end local function get(name) - if __proxy[name] then return __proxy[name] end - local dat = love.thread.getChannel(n..name):peek() - if type(dat)=="table" and dat[1]=="func" then - __proxy[name] = THREAD.loadDump(dat[2]) - return __proxy[name] - else - __proxy[name] = dat - return __proxy[name] - end + return unpackValue(love.thread.getChannel(n .. name):peek()) + -- if type(data) == "table" and data.init then + -- return data:init() + -- else + -- return data + -- end end return setmetatable({}, { @@ -240,11 +128,101 @@ function threads.createStaticTable(n) ) end -function threads.hold(n) - local dat - while not(dat) do - dat = n() +function INIT() + local GLOBAL, THREAD, DEFER = createTable("__GLOBAL__"), {}, {} + local status_channel, console_channel = love.thread.getChannel("__status_channel__" .. THREAD_ID), + love.thread.getChannel("__console_channel__") + + -- Non portable methods, shouldn't be used unless you know what you are doing + THREAD.packValue = packValue + THREAD.unpackValue = unpackValue + THREAD.createTable = createTable + + function THREAD.set(name, val) + GLOBAL[name] = val end + + function THREAD.get(name, val) + return GLOBAL[name] + end + + THREAD.waitFor = thread:newFunction(function(name) + local function wait() + math.randomseed(os.time()) + thread.yield() + end + repeat + wait() + until GLOBAL[name] ~= nil + return GLOBAL[name] + end, true) + + function THREAD.getCores() + return love.system.getProcessorCount() + end + + function THREAD.getConsole() + local c = {} + c.queue = console_channel + function c.print(...) + c.queue:push(table.concat(multi.pack(...), "\t")) + end + function c.error(err) + c.queue:push("Error in <"..THREAD_NAME..":" .. THREAD_ID .. ">: ".. err) + multi.error(err) + end + return c + end + + function THREAD.getThreads() + -- + end + + function THREAD.kill() -- trigger the lane destruction + error("Thread was killed!\1") + end + + function THREAD.pushStatus(...) + status_channel:push(multi.pack(...)) + end + + function THREAD.sleep(n) + love.timer.sleep(n) + end + + THREAD.hold = thread:newFunction(function(n) + thread.hold(n) + end, true) + + function THREAD.setENV(env, name) + GLOBAL[name or "__env"] = env + end + + function THREAD.getENV(name) + return GLOBAL[name or "__env"] + end + + function THREAD.exposeENV(name) + name = name or "__env" + local env = THREAD.getENV(name) + for i,v in pairs(env) do + _G[i] = v + end + end + + function THREAD.defer(func) + table.insert(DEFER, func) + end + + function THREAD.sync() + -- Maybe do something... + end + + return GLOBAL, THREAD, DEFER end -return threads \ No newline at end of file +return { + init = function() + return INIT() + end +} \ No newline at end of file diff --git a/integration/lovrManager/extensions.lua b/integration/lovrManager/extensions.lua index 7032b1d..ddc66c6 100644 --- a/integration/lovrManager/extensions.lua +++ b/integration/lovrManager/extensions.lua @@ -118,13 +118,13 @@ function multi:newSystemThreadedJobQueue(n) local rets link = c.OnJobCompleted(function(jid,...) if id==jid then - rets = {...} + rets = multi.pack(...) link:Destroy() end end) return thread.hold(function() if rets then - return unpack(rets) or multi.NIL + return multi.unpack(rets) or multi.NIL end end) end,holup),name @@ -134,7 +134,7 @@ function multi:newSystemThreadedJobQueue(n) thread.yield() local dat = c.queueReturn:pop() if dat then - c.OnJobCompleted:Fire(unpack(dat)) + c.OnJobCompleted:Fire(multi.unpack(dat)) end end end) @@ -152,6 +152,7 @@ function multi:newSystemThreadedJobQueue(n) local lastProc = clock() local queueAll = lovr.thread.getChannel("__JobQueue_"..jqc.."_queueAll") local registry = {} + _G["__QR"] = queueReturn setmetatable(_G,{__index = funcs}) thread:newThread("startUp",function() while true do @@ -177,7 +178,7 @@ function multi:newSystemThreadedJobQueue(n) lastProc = os.clock() local name = table.remove(dat,1) local id = table.remove(dat,1) - local tab = {funcs[name](unpack(dat))} + local tab = {funcs[name](multi.unpack(dat))} table.insert(tab,1,id) queueReturn:push(tab) end diff --git a/integration/lovrManager/init.lua b/integration/lovrManager/init.lua index 53cafa3..3d6943d 100644 --- a/integration/lovrManager/init.lua +++ b/integration/lovrManager/init.lua @@ -36,12 +36,14 @@ __THREADNAME__=table.remove(__IMPORTS,1) stab = THREAD.createStaticTable(__THREADNAME__) GLOBAL = THREAD.getGlobal() multi, thread = require("multi").init() -stab["returns"] = {THREAD.loadDump(__FUNC__)(unpack(__IMPORTS))} +stab["returns"] = {THREAD.loadDump(__FUNC__)(multi.unpack(__IMPORTS))} ]] local multi, thread = require("multi.compat.lovr2d"):init() local THREAD = {} __THREADID__ = 0 __THREADNAME__ = "MainThread" +_G.THREAD_NAME = "MAIN_THREAD" +_G.THREAD_ID = 0 multi.integration={} multi.integration.lovr2d={} local THREAD = require("multi.integration.lovrManager.threads") @@ -58,7 +60,7 @@ function THREAD:newFunction(func,holup) if t.stab["returns"] then local dat = t.stab.returns t.stab.returns = nil - return unpack(dat) + return multi.unpack(dat) end end) end,holup)() @@ -74,16 +76,23 @@ function multi:newSystemThread(name,func,...) GLOBAL["__THREAD_"..c.ID] = {ID=c.ID,Name=c.name,Thread=c.thread} GLOBAL["__THREAD_COUNT"] = THREAD_ID THREAD_ID=THREAD_ID+1 + + if self.isActor then + self:create(c) + else + multi.create(multi, c) + end + return c end THREAD.newSystemThread = multi.newSystemThread function lovr.threaderror(thread, errorstr) - print("Thread error!\n"..errorstr) + multi.print("Thread error!\n"..errorstr) end multi.integration.GLOBAL = GLOBAL multi.integration.THREAD = THREAD require("multi.integration.lovrManager.extensions") -print("Integrated lovr Threading!") +multi.print("Integrated lovr Threading!") return {init=function() return GLOBAL,THREAD end} \ No newline at end of file diff --git a/integration/lovrManager/threads.lua b/integration/lovrManager/threads.lua index 6a95a1e..dc919ab 100644 --- a/integration/lovrManager/threads.lua +++ b/integration/lovrManager/threads.lua @@ -156,7 +156,7 @@ function threads.getConsole() local c = {} c.queue = lovr.thread.getChannel("__CONSOLE__") function c.print(...) - c.queue:push{...} + c.queue:push(multi.pack(...)) end function c.error(err) c.queue:push{"ERROR in <"..__THREADNAME__..">: "..err,__THREADID__} @@ -174,7 +174,7 @@ if not ISTHREAD then dat = queue:pop() if dat then lastproc = clock() - print(unpack(dat)) + print(multi.unpack(dat)) end if clock()-lastproc>2 then thread.sleep(.1) diff --git a/integration/luvitManager.lua b/integration/luvitManager.lua index 0f53cb9..46a0dc2 100644 --- a/integration/luvitManager.lua +++ b/integration/luvitManager.lua @@ -35,7 +35,7 @@ local function _INIT(luvitThread, timer) end -- Step 1 get setup threads on luvit... Sigh how do i even... local multi, thread = require("multi").init() - isMainThread = true + multi.isMainThread = true function multi:canSystemThread() return true end @@ -107,7 +107,7 @@ local function _INIT(luvitThread, timer) local c = {} local __self = c c.name = name - c.Type = "sthread" + c.Type = multi.STHREAD c.thread = {} c.func = string.dump(func) function c:kill() diff --git a/integration/pesudoManager/extensions.lua b/integration/pesudoManager/extensions.lua deleted file mode 100644 index 3881f3f..0000000 --- a/integration/pesudoManager/extensions.lua +++ /dev/null @@ -1,141 +0,0 @@ ---[[ -MIT License - -Copyright (c) 2022 Ryan Ward - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sub-license, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -]] -local multi, thread = require("multi"):init() -local GLOBAL, THREAD = multi.integration.GLOBAL,multi.integration.THREAD - -local function stripUpValues(func) - local dmp = string.dump(func) - if setfenv then - return loadstring(dmp,"IsolatedThread_PesudoThreading") - else - return load(dmp,"IsolatedThread_PesudoThreading","bt") - end -end - -function multi:newSystemThreadedQueue(name) - local c = {} - function c:push(v) - table.insert(self,v) - end - function c:pop() - return table.remove(self,1) - end - function c:peek() - return self[1] - end - function c:init() - return self - end - GLOBAL[name or "_"] = c - return c -end -function multi:newSystemThreadedTable(name) - local c = {} - function c:init() - return self - end - GLOBAL[name or "_"] = c - return c -end -local setfenv = setfenv -if not setfenv then - if not debug then - multi.print("Unable to implement setfenv in lua 5.2+ the debug module is not available!") - else - setfenv = function(f, env) - return load(string.dump(f), nil, nil, env) - end - end -end -function multi:newSystemThreadedJobQueue(n) - local c = {} - c.cores = n or THREAD.getCores()*2 - c.OnJobCompleted = multi:newConnection() - local jobs = {} - local ID=1 - local jid = 1 - local env = {} - setmetatable(env,{ - __index = _G - }) - local funcs = {} - function c:doToAll(func) - setfenv(func,env)() - return self - end - function c:registerFunction(name,func) - funcs[name] = setfenv(func,env) - return self - end - function c:pushJob(name,...) - table.insert(jobs,{name,jid,{...}}) - jid = jid + 1 - return jid-1 - end - function c:isEmpty() - print(#jobs) - return #jobs == 0 - end - local nFunc = 0 - function c:newFunction(name,func,holup) -- This registers with the queue - local func = stripUpValues(func) - if type(name)=="function" then - holup = func - func = name - name = "JQ_Function_"..nFunc - end - nFunc = nFunc + 1 - c:registerFunction(name,func) - return thread:newFunction(function(...) - local id = c:pushJob(name,...) - local link - local rets - link = c.OnJobCompleted(function(jid,...) - if id==jid then - rets = {...} - link:Destroy() - end - end) - return thread.hold(function() - if rets then - return unpack(rets) or multi.NIL - end - end) - end,holup),name - end - for i=1,c.cores do - thread:newthread("PesudoThreadedJobQueue_"..i,function() - while true do - thread.yield() - if #jobs>0 then - local j = table.remove(jobs,1) - c.OnJobCompleted:Fire(j[2],funcs[j[1]](unpack(j[3]))) - else - thread.sleep(.05) - end - end - end) - end - return c -end \ No newline at end of file diff --git a/integration/priorityManager/init.lua b/integration/priorityManager/init.lua new file mode 100644 index 0000000..4c09007 --- /dev/null +++ b/integration/priorityManager/init.lua @@ -0,0 +1,213 @@ +-- Advanced process management. Mutates the multi namespace +local multi, thread = require("multi"):init() +local ok, chronos = pcall(require, "chronos") -- hpc + +if not ok then chronos = nil end + +-- This is an integration, we cannot directly access locals that are in the main file. + +local PList = { + multi.Priority_Core, + multi.Priority_Very_High, + multi.Priority_High, + multi.Priority_Above_Normal, + multi.Priority_Normal, + multi.Priority_Below_Normal, + multi.Priority_Low, + multi.Priority_Very_Low, + multi.Priority_Idle +} + +-- Restructered these functions since they rely on local variables from the core library + +local mainloop = multi.mainloopRef +local mainloop_p = multi.mainloop_p +local uManagerRef = multi.uManagerRef +local uManagerRefP = multi.uManagerRefP1 + +local PROFILE_COUNT = 5 + +-- self:setCurrentProcess() a bit slower than using the local var, but there isn't another option + +local priorityManager = multi:newProcessor("Priority Manager", true) +priorityManager.newThread = function() multi.warn("You cannot spawn threads on the priority manager!") end + +priorityManager.setPriorityScheme = function() multi.warn("You cannot set priority on the priorityManager!") end + +local function average(t) + local sum = 0 + for _,v in pairs(t) do + sum = sum + v + end + return sum / #t +end + +local function getPriority(obj) + local avg = average(obj.__profiling) + if avg < 0.0002 then + return PList[1] + elseif avg < 0.0004 then + return PList[2] + elseif avg < 0.0008 then + return PList[3] + elseif avg < 0.001 then + return PList[4] + elseif avg < 0.0025 then + return PList[5] + elseif avg < 0.005 then + return PList[6] + elseif avg < 0.008 then + return PList[7] + elseif avg < 0.01 then + return PList[8] + else + return PList[9] + end +end + +local start, stop + +priorityManager.uManager = function(self) + -- proc.run already checks if the processor is active + self:setCurrentProcess() + local Loop=self.Mainloop + local ctask + for _D=#Loop,1,-1 do + ctask = Loop[_D] + ctask:setCurrentTask() + start = chronos.nanotime() + if ctask:Act() then + stop = chronos.nanotime() + if ctask.__profiling then + table.insert(ctask.__profiling, stop - start) + end + if ctask.__profiling and #ctask.__profiling == PROFILE_COUNT then + ctask:setPriority(getPriority(ctask)) + ctask:reallocate(ctask.__restoreProc) + ctask.__restoreProc = nil + ctask.__profiling = nil + end + end + self:setCurrentProcess() + end +end + +local function processHook(obj, proc) + if obj.Type == multi.registerType("process", "processes") or not(obj.IsAnActor) then return end + obj.__restoreProc = proc + obj.__profiling = {} + obj:reallocate(priorityManager) +end + +local function init() + local registry = {} + + multi.priorityScheme = { + RoundRobin = "RoundRobin", + PriorityBased = "PriorityBased", + TimeBased = "TimeBased" + } + + function multi:setProfilerCount(count) + PROFILE_COUNT = count + end + + function multi:recalibrate() + if self.__processConn then + local items = self.Mainloop + for i,v in pairs(items) do + processHook(v, self) + end + else + multi.error("Cannot recalibrate the priority if not using Time based mangement!") + end + end + + function multi:isRegistredScheme(scheme) + return registry[name] ~= nil + end + + function multi:getRegisteredScheme(scheme) + return registry[name].mainloop, registry[name].umanager, registry[name].condition + end + + local empty_func = function() return true end + function multi:registerScheme(name,options) + local mainloop = options.mainloop or multi.error("You must provide a mainloop option when registring a scheme!") + local umanager = options.umanager or multi.error("You must provide a umanager option when registring a scheme!") + + if not options.condition then + multi.warn("You might want to use condition when registring a scheme! A function that returns true has been auto generated for you!") + end + + local condition = options.condition or empty_func + + if registry[name] and not registry[name].static then + multi.warn("A scheme named: \"" .. name .. "\" has already been registred, overriting!") + else + multi.error("A scheme named: \"" .. name .. "\" has already been registred!") + end + + registry[name] = { + mainloop = mainloop, + umanager = umanger, + condition = condition, + static = options.static or false + } + + multi.priorityScheme[name] = name + + return true + end + + function multi:setPriorityScheme(scheme) + + if not self.Type == multi.registerType("process", "processes") or not self.Type == multi.registerType("rootprocess") then + multi.warn("You should only invoke setPriorityScheme on a processor object!") + end + + if scheme == multi.priorityScheme.RoundRobin then + if self.__processConn then self.OnObjectCreated:Unconnect(self.__processConn) self.__processConn = nil end + self.mainloop = mainloop + self.uManager = uManagerRef + elseif scheme == multi.priorityScheme.PriorityBased then + if self.__processConn then self.OnObjectCreated:Unconnect(self.__processConn) self.__processConn = nil end + self.mainloop = mainloop_p + self.uManager = uManagerRefP + elseif scheme == multi.priorityScheme.TimeBased then + if not chronos then return multi.warn("Unable to use TimeBased Priority without the chronos library!") end + if self.__processConn then multi.warn("Already enabled TimeBased Priority!") end + self.__processConn = self.OnObjectCreated(processHook) + self.mainloop = mainloop_p + self.uManager = uManagerRefP + elseif self:isRegistredScheme(scheme) then + local mainloop, umanager, condition = self:getRegisteredScheme(scheme) + if condition() then + self.mainloop = mainloop + self.uManager = umanager + end + else + self.error("Invalid priority scheme selected!") + end + + end +end + +local function init_chronos() + -- Let's implement a higher precision clock + multi.setClock(chronos.nanotime) -- This is also in .000 format. So a plug and play works. + thread:newThread("System Priority Manager", function() + while true do + thread.yield() + priorityManager.run() + end + end) +end + +if chronos then + init_chronos() +else + multi.warn("In order to have time based priority management, you need to install the chronos library!") +end + +init() \ No newline at end of file diff --git a/integration/pseudoManager/extensions.lua b/integration/pseudoManager/extensions.lua new file mode 100644 index 0000000..00ddfa3 --- /dev/null +++ b/integration/pseudoManager/extensions.lua @@ -0,0 +1,221 @@ +--[[ +MIT License + +Copyright (c) 2022 Ryan Ward + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] +local multi, thread = require("multi"):init() +local GLOBAL, THREAD = multi.integration.GLOBAL, multi.integration.THREAD + +local function stripUpValues(func) + local dmp = string.dump(func) + if setfenv then + return loadstring(dmp,"IsolatedThread_PesudoThreading") + else + return load(dmp,"IsolatedThread_PesudoThreading","bt") + end +end + +function multi:newSystemThreadedQueue(name) + local c = {} + c.data = {} + c.Type = multi.registerType("s_queue") + function c:push(v) + table.insert(self.data,v) + end + function c:pop() + return table.remove(self.data,1) + end + function c:peek() + return self.data[1] + end + function c:init() + return self + end + function c:Hold(opt) + if opt.peek then + return thread.hold(function() + return self:peek() + end) + else + return thread.hold(function() + return self:pop() + end) + end + end + GLOBAL[name or "_"] = c + return c +end + +function multi:newSystemThreadedTable(name) + local c = {} + c.Type = multi.registerType("s_table") + function c:init() + return self + end + function c:Hold(opt) + if opt.key then + return thread.hold(function() + return self.tab[opt.key] + end) + else + multi.error("Must provide a key to check opt.key = 'key'") + end + end + GLOBAL[name or "_"] = c + return c +end + +local setfenv = multi.isolateFunction + +local jqc = 1 +function multi:newSystemThreadedJobQueue(n) + local c = {} + + c.cores = n or THREAD.getCores() + c.registerQueue = {} + c.Type = multi.registerType("s_jobqueue") + c.funcs = multi:newSystemThreadedTable("__JobQueue_"..jqc.."_table") + c.queue = multi:newSystemThreadedQueue("__JobQueue_"..jqc.."_queue") + c.queueReturn = multi:newSystemThreadedQueue("__JobQueue_"..jqc.."_queueReturn") + c.queueAll = multi:newSystemThreadedQueue("__JobQueue_"..jqc.."_queueAll") + c.id = 0 + c.OnJobCompleted = multi:newConnection() + + local allfunc = 0 + + function c:doToAll(func) + for i = 1, self.cores do + self.queueAll:push({allfunc, func}) + end + allfunc = allfunc + 1 + end + function c:registerFunction(name, func) + if self.funcs[name] then + multi.error("A function by the name "..name.." has already been registered!") + end + self.funcs[name] = func + end + function c:pushJob(name,...) + self.id = self.id + 1 + self.queue:push{name,self.id,...} + return self.id + end + function c:isEmpty() + return queueJob:peek()==nil + end + local nFunc = 0 + function c:newFunction(name,func,holup) -- This registers with the queue + if type(name)=="function" then + holup = func + func = name + name = "JQ_Function_"..nFunc + end + nFunc = nFunc + 1 + c:registerFunction(name,func) + return thread:newFunction(function(...) + local id = c:pushJob(name,...) + local link + local rets + link = c.OnJobCompleted(function(jid,...) + if id==jid then + rets = multi.pack(...) + end + end) + return thread.hold(function() + if rets then + return multi.unpack(rets) or multi.NIL + end + end) + end,holup),name + end + thread:newThread("jobManager",function() + while true do + thread.yield() + local dat = c.queueReturn:pop() + if dat then + c.OnJobCompleted:Fire(multi.unpack(dat)) + end + end + end) + for i=1,c.cores do + multi:newSystemThread("STJQ_"..multi.randomString(8),function(jqc) + local GLOBAL, THREAD = require("multi.integration.pseudoManager"):init() + local multi, thread = require("multi"):init() + local clock = os.clock + local funcs = THREAD.waitFor("__JobQueue_"..jqc.."_table") + local queue = THREAD.waitFor("__JobQueue_"..jqc.."_queue") + local queueReturn = THREAD.waitFor("__JobQueue_"..jqc.."_queueReturn") + local queueAll = THREAD.waitFor("__JobQueue_"..jqc.."_queueAll") + local registry = {} + _G["__QR"] = queueReturn + setmetatable(_G,{__index = funcs}) + thread:newThread("startUp",function() + while true do + thread.yield() + local all = queueAll:peek() + if all and not registry[all[1]] then + queueAll:pop()[2]() + end + end + end) + thread:newThread("runner",function() + thread.sleep(.1) + while true do + thread.yield() + local all = queueAll:peek() + if all and not registry[all[1]] then + queueAll:pop()[2]() + end + local dat = thread.hold(queue) + if dat then + multi:newThread("JobSubRunner",function() + local name = table.remove(dat,1) + local id = table.remove(dat,1) + local tab = {multi.isolateFunction(funcs[name],_G)(multi.unpack(dat))} + table.insert(tab,1,id) + queueReturn:push(tab) + end) + end + end + end) + multi:mainloop() + end, jqc) + end + + function c:Hold(opt) + return thread.hold(self.OnJobCompleted) + end + + jqc = jqc + 1 + + self:create(c) + + return c +end + +function multi:newSystemThreadedConnection(name) + local conn = multi:newConnection() + conn.init = function(self) return self end + GLOBAL[name or "_"] = conn + return conn +end + +require("multi.integration.sharedExtensions") \ No newline at end of file diff --git a/integration/pesudoManager/init.lua b/integration/pseudoManager/init.lua similarity index 70% rename from integration/pesudoManager/init.lua rename to integration/pseudoManager/init.lua index 84bdca3..46c4c9b 100644 --- a/integration/pesudoManager/init.lua +++ b/integration/pseudoManager/init.lua @@ -24,6 +24,8 @@ SOFTWARE. package.path = "?/init.lua;?.lua;" .. package.path local multi, thread = require("multi"):init() +local pseudoProcessor = multi:newProcessor() + if multi.integration then return { init = function() @@ -31,8 +33,12 @@ if multi.integration then end } end +multi.isMainThread = true +local activator = require("multi.integration.pseudoManager.threads") +local GLOBAL, THREAD = activator.init(thread) -local GLOBAL, THREAD = require("multi.integration.pesudoManager.threads").init(thread) +_G.THREAD_NAME = "MAIN_THREAD" +_G.THREAD_ID = 0 function multi:canSystemThread() -- We are emulating system threading return true @@ -50,29 +56,44 @@ local function split(str) return tab end -local tab = [[_VERSION,io,os,require,load,debug,assert,collectgarbage,error,getfenv,getmetatable,ipairs,loadstring,module,next,pairs,pcall,print,rawequal,rawget,rawset,select,setfenv,setmetatable,tonumber,tostring,type,unpack,xpcall,math,coroutine,string,table]] +local tab = [[_VERSION,io,os,require,load,debug,assert,collectgarbage,error,getfenv,getmetatable,ipairs,loadstring,module,next,pairs,pcall,print,rawequal,rawget,rawset,select,setfenv,setmetatable,tonumber,tostring,type,xpcall,math,coroutine,string,table]] tab = split(tab) local id = 0 -function multi:newSystemThread(name,func,...) - GLOBAL["$THREAD_NAME"] = name - GLOBAL["$__THREADNAME__"] = name - GLOBAL["$THREAD_ID"] = id - GLOBAL["$thread"] = thread - local env = { + +function multi:newSystemThread(name, func, ...) + local env + env = { GLOBAL = GLOBAL, THREAD = THREAD, - THREAD_NAME = name, - __THREADNAME__ = name, + THREAD_NAME = tostring(name), + __THREADNAME__ = tostring(name), THREAD_ID = id, - thread = thread + thread = thread, + multi = multi, } - - for i = 1,#tab do - env[tab[i]] = _G[tab[i]] + + for i, v in pairs(_G) do + if not(env[i]) and not(i == "_G") and not(i == "local_global") then + env[i] = v + else + multi.warn("skipping:",i) + end end - local th = thread:newISOThread(name,func,env,...) + if GLOBAL["__env"] then + for i,v in pairs(GLOBAL["__env"]) do + env[i] = v + end + end + + env._G = env + + local GLOBAL, THREAD = activator.init(thread, env) + + local th = pseudoProcessor:newISOThread(name, func, env, ...) + th.Type = multi.registerType("s_thread", "pseudoThreads") + th.OnError(multi.error) id = id + 1 @@ -86,14 +107,15 @@ THREAD.newSystemThread = multi.newSystemThread function THREAD:newFunction(func,holdme) return thread:newFunctionBase(function(...) return multi:newSystemThread("TempSystemThread",func,...) - end,holdme)() + end, holdme, multi.registerType("s_function", "pseudoFunctions"))() end multi.print("Integrated Pesudo Threading!") multi.integration = {} -- for module creators multi.integration.GLOBAL = GLOBAL multi.integration.THREAD = THREAD -require("multi.integration.pesudoManager.extensions") +require("multi.integration.pseudoManager.extensions") +require("multi.integration.sharedExtensions") return { init = function() return GLOBAL, THREAD diff --git a/integration/pesudoManager/threads.lua b/integration/pseudoManager/threads.lua similarity index 81% rename from integration/pesudoManager/threads.lua rename to integration/pseudoManager/threads.lua index b8ee6aa..5490607 100644 --- a/integration/pesudoManager/threads.lua +++ b/integration/pseudoManager/threads.lua @@ -33,6 +33,7 @@ end local function INIT(thread) local THREAD = {} local GLOBAL = {} + THREAD.Priority_Core = 3 THREAD.Priority_High = 2 THREAD.Priority_Above_Normal = 1 @@ -80,31 +81,42 @@ local function INIT(thread) THREAD.pushStatus = thread.pushStatus - if os.getOS() == "windows" then - THREAD.__CORES = tonumber(os.getenv("NUMBER_OF_PROCESSORS")) - else - THREAD.__CORES = tonumber(io.popen("nproc --all"):read("*n")) - end - function THREAD.kill() error("Thread was killed!") end - function THREAD.getName() - return GLOBAL["$THREAD_NAME"] - end - - function THREAD.getID() - return GLOBAL["$THREAD_ID"] - end - THREAD.sleep = thread.sleep THREAD.hold = thread.hold + THREAD.defer = thread.defer + + function THREAD.setENV(env, name) + name = name or "__env" + GLOBAL[name] = env + end + + function THREAD.getENV(name) + name = name or "__env" + return GLOBAL[name] + end + + function THREAD.exposeENV(name) + name = name or "__env" + local env = THREAD.getENV(name) + for i,v in pairs(env) do + -- This may need to be reworked! + local_global[i] = v + end + end + + function THREAD.sync() + thread.sleep(.5) + end + return GLOBAL, THREAD end -return {init = function(thread) - return INIT(thread) +return {init = function(thread, global) + return INIT(thread, global) end} \ No newline at end of file diff --git a/integration/sharedExtensions/init.lua b/integration/sharedExtensions/init.lua new file mode 100644 index 0000000..01bae7f --- /dev/null +++ b/integration/sharedExtensions/init.lua @@ -0,0 +1,336 @@ +--[[ todo finish the targeted job! +MIT License + +Copyright (c) 2023 Ryan Ward + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sub-license, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + +local multi, thread = require("multi"):init() + +-- Returns a handler that allows a user to interact with an object on another thread! +-- Create on the thread that you want to interact with, send over the handle + +function multi:chop(obj) + if not _G["UIDS"] then + _G["UIDS"] = {} + end + local multi, thread = require("multi"):init() + local list = {[0] = multi.randomString(12)} + _G[list[0]] = obj + for i,v in pairs(obj) do + if type(v) == "function" or type(v) == "table" and v.Type == multi.registerType("s_function") then + table.insert(list, i) + elseif type(v) == "table" and v.Type == multi.registerType("connector", "connections") then + table.insert(list, {i, multi:newProxy(multi:chop(v)):init()}) + end + end + return list +end + +function multi:newProxy(list) + + local c = {} + + c.name = multi.randomString(12) + c.is_init = false + local multi, thread = nil, nil + function c:init() + local multi, thread = nil, nil + local function copy(obj) + if type(obj) ~= 'table' then return obj end + local res = {} + for k, v in pairs(obj) do res[copy(k)] = copy(v) end + return res + end + if not(self.is_init) then + THREAD.sync() + self.is_init = true + local multi, thread = require("multi"):init() + self.proxy_link = "PL" .. multi.randomString(12) + + if multi.integration then + GLOBAL = multi.integration.GLOBAL + THREAD = multi.integration.THREAD + end + + GLOBAL[self.proxy_link] = self + + local function check() + return self.send:pop() + end + + self.send = multi:newSystemThreadedQueue(self.name.."_S"):init() + self.recv = multi:newSystemThreadedQueue(self.name.."_R"):init() + self.funcs = list + self._funcs = copy(list) + self.Type = multi.registerType("proxy", "proxies") + self.TID = THREAD_ID + + thread:newThread("Proxy_Handler_" .. multi.randomString(4), function() + while true do + local data = thread.hold(check) + if data then + -- Let's not hold the main threadloop + thread:newThread("Temp_Thread", function() + local func = table.remove(data, 1) + local sref = table.remove(data, 1) + local ret + if sref then + ret = {_G[list[0]][func](_G[list[0]], multi.unpack(data))} + else + ret = {_G[list[0]][func](multi.unpack(data))} + end + + for i = 1,#ret do + if type(ret[i]) == "table" and ret[i].Type ~= nil and ret[i].Type ~= multi.registerType("proxy", "proxies") then + ret[i] = "\1PARENT_REF" + end + if type(ret[i]) == "table" and getmetatable(ret[i]) then + setmetatable(ret[i],nil) -- remove that metatable, we do not need it on the other side! + end + if ret[i] == _G[list[0]] then + -- We cannot return itself, that return can contain bad values. + ret[i] = "\1SELF_REF" + end + end + table.insert(ret, 1, func) + self.recv:push(ret) + end) + end + end + end) + return self + else + THREAD.sync() + if not self.funcs then return self end + local function copy(obj) + if type(obj) ~= 'table' then return obj end + local res = {} + for k, v in pairs(obj) do res[copy(k)] = copy(v) end + return res + end + local multi, thread = require("multi"):init() + local me = self + local funcs = copy(self.funcs) + if multi.integration then + GLOBAL = multi.integration.GLOBAL + THREAD = multi.integration.THREAD + end + self.send = THREAD.waitFor(self.name.."_S"):init() + self.recv = THREAD.waitFor(self.name.."_R"):init() + self.Type = multi.registerType("proxy", "proxies") + for _,v in pairs(funcs) do + if type(v) == "table" then + -- We have a connection + v[2]:init(proc_name) + self[v[1]] = v[2] + v[2].Parent = self + setmetatable(v[2],getmetatable(multi:newConnection())) + else + self[v] = thread:newFunction(function(self,...) + if self == me then + me.send:push({v, true, ...}) + else + me.send:push({v, false, ...}) + end + return thread.hold(function() + local data = me.recv:peek() + if data and data[1] == v then + me.recv:pop() + table.remove(data, 1) + for i=1,#data do + if data[i] == "\1SELF_REF" then + data[i] = me + elseif data[i] == "\1PARENT_REF" then + data[i] = me.Parent + end + end + return multi.unpack(data) + end + end) + end, true) + end + end + return self + end + end + function c:getTransferable() + local cp = {} + local multi, thread = require("multi"):init() + local function copy(obj) + if type(obj) ~= 'table' then return obj end + local res = {} + for k, v in pairs(obj) do res[copy(k)] = copy(v) end + return res + end + cp.is_init = true + cp.proxy_link = self.proxy_link + cp.name = self.name + cp.funcs = copy(self._funcs) + cp.init = function(self) + local multi, thread = require("multi"):init() + if multi.integration then + GLOBAL = multi.integration.GLOBAL + THREAD = multi.integration.THREAD + end + local proxy = THREAD.waitFor(self.proxy_link) + proxy.funcs = self.funcs + return proxy:init() + end + return cp + end + self:create(c) + return c +end + +local targets = {} +local references = {} + +local jid = -1 +function multi:newSystemThreadedProcessor(cores) + + local name = "STP_"..multi.randomString(4) -- set a random name if none was given. + + local autoscale = autoscale or false -- Will scale up the number of cores that the process uses. + local c = {} + + setmetatable(c,{__index = multi}) + + c.Type = multi.registerType("s_process", "s_processes") + c.threads = {} + c.cores = cores or 8 + c.Name = name + c.Mainloop = {} + c.__count = 0 + c.processors = {} + c.proc_list = {} + c.OnObjectCreated = multi:newConnection() + c.parent = self + c.jobqueue = multi:newSystemThreadedJobQueue(c.cores) + + function c:pushJob(ID, name, ...) + local tq = THREAD.waitFor(self.Name .. "_target_tq_" .. ID):init() + tq:push{name, jid, multi.pack(...)} + jid = jid - 1 + return jid + 1 + end + + c.jobqueue:registerFunction("packObj",function(obj) + local multi, thread = require("multi"):init() + + local list = multi:chop(obj) + obj.__link_name = list[0] + + local proxy = multi:newProxy(list):init() + + return proxy + end) + + c.spawnThread = c.jobqueue:newFunction("__spawnThread__", function(name, func, ...) + local multi, thread = require("multi"):init() + local obj = thread:newThread(name, func, ...) + return packObj(obj) + end, true) + + c.spawnTask = c.jobqueue:newFunction("__spawnTask__", function(obj, func, ...) + local multi, thread = require("multi"):init() + local obj = multi[obj](multi, func, ...) + return packObj(obj) + end, true) + + local implement = { + "newLoop", + "newTLoop", + "newUpdater", + "newEvent", + "newAlarm", + "newStep", + "newTStep", + "newService" + } + + for _, method in pairs(implement) do + c[method] = thread:newFunction(function(self, ...) + proxy = self.spawnTask(method, ...) + proxy:init() + references[proxy] = self + return proxy + end, true) + end + + function c:newThread(name, func, ...) + proxy = self.spawnThread(name, func, ...):init(self.Name) + references[proxy] = self + table.insert(self.threads, proxy) + return proxy + end + + function c:newFunction(func, holdme) + return self.jobqueue:newFunction(func, holdme) + end + + function c:newSharedTable(name) + if not name then multi.error("You must provide a name when creating a table!") end + local tbl_name = "TABLE_"..multi.randomString(8) + self.jobqueue:doToAll(function(tbl_name, interaction) + _G[interaction] = THREAD.waitFor(tbl_name):init() + end, tbl_name, name) + return multi:newSystemThreadedTable(tbl_name):init() + end + + function c:getHandler() + return function() end -- return empty function + end + + function c:getThreads() + return self.threads + end + + function c:getFullName() + return self.parent:getFullName() .. "." .. c.Name + end + + function c:getName() + return self.Name + end + + function c.run() + return self + end + + function c.isActive() + return true + end + + function c.Start() + return self + end + + function c.Stop() + return self + end + + function c:Destroy() + return false + end + + return c +end + diff --git a/lovethreads/conf.lua b/lovethreads/conf.lua new file mode 100644 index 0000000..c202fa9 --- /dev/null +++ b/lovethreads/conf.lua @@ -0,0 +1,20 @@ +function love.conf(t) + t.identity = nil -- The name of the save directory (string) + t.version = "12.0" -- The LOVE version this game was made for (string) + t.console = true -- Attach a console (boolean, Windows only) + + -- t.modules.audio = false -- Enable the audio module (boolean) + -- t.modules.event = false -- Enable the event module (boolean) + -- t.modules.graphics = false -- Enable the graphics module (boolean) + -- t.modules.image = false -- Enable the image module (boolean) + -- t.modules.joystick = false -- Enable the joystick module (boolean) + -- t.modules.keyboard = false -- Enable the keyboard module (boolean) + -- t.modules.math = false -- Enable the math module (boolean) + -- t.modules.mouse = false -- Enable the mouse module (boolean) + -- t.modules.physics = false -- Enable the physics module (boolean) + -- t.modules.sound = false -- Enable the sound module (boolean) + -- t.modules.system = true -- Enable the system module (boolean) + -- t.modules.timer = true -- Enable the timer module (boolean) + -- t.modules.window = false -- Enable the window module (boolean) + -- t.modules.thread = true -- Enable the thread module (boolean) +end diff --git a/lovethreads/main.lua b/lovethreads/main.lua new file mode 100644 index 0000000..8936ea3 --- /dev/null +++ b/lovethreads/main.lua @@ -0,0 +1,75 @@ +package.path = "../?/init.lua;../?.lua;"..package.path +local multi, thread = require("multi"):init{print=true, warning = true, error=true} +local utils = require("multi.integration.loveManager.utils") + +local people = {1,2,3} + +function dump(o) + if type(o) == 'table' then + local s = '{ ' + for k,v in pairs(o) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s .. '['..k..'] = ' .. dump(v) .. '('..type(v):sub(1,1)..'),' + end + return s .. '} ' + else + return tostring(o) + end +end + +local fpeople = utils.pack(people) + +print("Pack:", dump(fpeople)) + +local people = utils.unpack(fpeople) + +print("Unpack:", dump(people)) +print(type(people[3])) + +-- GLOBAL, THREAD = require("multi.integration.loveManager"):init() + +-- local queue = multi:newSystemThreadedQueue("TestQueue") +-- local tab = multi:newSystemThreadedTable("TestTable") + +-- local test = multi:newSystemThread("Test",function() +-- local queue = THREAD.waitFor("TestQueue") +-- local tab = THREAD.waitFor("TestTable") +-- print("THREAD_ID:",THREAD_ID) +-- queue:push("Did it work?") +-- tab["Test"] = true +-- return 1,2,3 +-- end) + +-- multi:newThread("QueueTest", function() +-- print(thread.hold(queue)) +-- print(thread.hold(tab, {key="Test"})) +-- print("Done!") +-- end) + +-- local jq = multi:newSystemThreadedJobQueue(n) + +-- jq:registerFunction("test2",function() +-- print("This works!") +-- end) + +-- jq:registerFunction("test",function(a, b, c) +-- print(a, b+c) +-- test2() +-- return a+b+c +-- end) + +-- print("Job:",jq:pushJob("test",1,2,3)) +-- print("Job:",jq:pushJob("test",2,3,4)) +-- print("Job:",jq:pushJob("test",5,6,7)) + +-- jq.OnJobCompleted(function(...) +-- print("Job Completed!", ...) +-- end) + +-- function love.draw() +-- -- +-- end + +-- function love.update() +-- multi:uManager() +-- end \ No newline at end of file diff --git a/lovethreads/multi b/lovethreads/multi new file mode 100644 index 0000000..b870225 --- /dev/null +++ b/lovethreads/multi @@ -0,0 +1 @@ +../ \ No newline at end of file diff --git a/rockspecs/multi-16.0-0.rockspec b/rockspecs/multi-16.0-0.rockspec new file mode 100644 index 0000000..b44f1f9 --- /dev/null +++ b/rockspecs/multi-16.0-0.rockspec @@ -0,0 +1,42 @@ +package = "multi" +version = "16.0-0" +source = { + url = "git://github.com/rayaman/multi.git", + tag = "v16.0.0", +} +description = { + summary = "Lua Multi tasking library", + detailed = [[ + This library contains many methods for multi tasking. Features non coroutine based multi-tasking, coroutine based multi-tasking, and system threading (Requires use of an integration). + Check github for documentation. + ]], + homepage = "https://github.com/rayaman/multi", + license = "MIT" +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + ["multi"] = "init.lua", + ["multi.integration.lanesManager"] = "integration/lanesManager/init.lua", + ["multi.integration.lanesManager.extensions"] = "integration/lanesManager/extensions.lua", + ["multi.integration.lanesManager.threads"] = "integration/lanesManager/threads.lua", + ["multi.integration.loveManager"] = "integration/loveManager/init.lua", + ["multi.integration.loveManager.extensions"] = "integration/loveManager/extensions.lua", + ["multi.integration.loveManager.threads"] = "integration/loveManager/threads.lua", + ["multi.integration.loveManager.utils"] = "integration/loveManager/threads.lua", + --["multi.integration.lovrManager"] = "integration/lovrManager/init.lua", + --["multi.integration.lovrManager.extensions"] = "integration/lovrManager/extensions.lua", + --["multi.integration.lovrManager.threads"] = "integration/lovrManager/threads.lua", + ["multi.integration.pseudoManager"] = "integration/pseudoManager/init.lua", + ["multi.integration.pseudoManager.extensions"] = "integration/pseudoManager/extensions.lua", + ["multi.integration.pseudoManager.threads"] = "integration/pseudoManager/threads.lua", + ["multi.integration.luvitManager"] = "integration/luvitManager.lua", + ["multi.integration.threading"] = "integration/threading.lua", + ["multi.integration.sharedExtensions"] = "integration/sharedExtensions/init.lua", + ["multi.integration.priorityManager"] = "integration/priorityManager/init.lua", + --["multi.integration.networkManager"] = "integration/networkManager.lua", + } +} \ No newline at end of file diff --git a/tests/conf.lua b/tests/conf.lua new file mode 100644 index 0000000..4d2c4ee --- /dev/null +++ b/tests/conf.lua @@ -0,0 +1,39 @@ +function love.conf(t) + t.identity = nil -- The name of the save directory (string) + t.version = "12.0" -- The LOVE version this game was made for (string) + t.console = true -- Attach a console (boolean, Windows only) + + t.window.title = "MultiThreadTest" -- The window title (string) + t.window.icon = nil -- Filepath to an image to use as the window's icon (string) + t.window.width = 1280 -- The window width (number) + t.window.height = 720 -- The window height (number) + + t.window.borderless = false -- Remove all border visuals from the window (boolean) + t.window.resizable = true -- Let the window be user-resizable (boolean) + t.window.minwidth = 1 -- Minimum window width if the window is resizable (number) + t.window.minheight = 1 -- Minimum window height if the window is resizable (number) + t.window.fullscreen = false -- Enable fullscreen (boolean) + t.window.fullscreentype = "desktop" -- Standard fullscreen or desktop fullscreen mode (string) + t.window.vsync = false -- Enable vertical sync (boolean) + t.window.fsaa = 2 -- The number of samples to use with multi-sampled antialiasing (number) + t.window.display = 1 -- Index of the monitor to show the window in (number) + t.window.highdpi = false -- Enable high-dpi mode for the window on a Retina display (boolean) + t.window.srgb = false -- Enable sRGB gamma correction when drawing to the screen (boolean) + t.window.x = nil -- The x-coordinate of the window's position in the specified display (number) + t.window.y = nil -- The y-coordinate of the window's position in the specified display (number) + + t.modules.audio = false -- Enable the audio module (boolean) + t.modules.event = false -- Enable the event module (boolean) + t.modules.graphics = false -- Enable the graphics module (boolean) + t.modules.image = false -- Enable the image module (boolean) + t.modules.joystick = false -- Enable the joystick module (boolean) + t.modules.keyboard = false -- Enable the keyboard module (boolean) + t.modules.math = false -- Enable the math module (boolean) + t.modules.mouse = false -- Enable the mouse module (boolean) + t.modules.physics = false -- Enable the physics module (boolean) + t.modules.sound = false -- Enable the sound module (boolean) + t.modules.system = false -- Enable the system module (boolean) + t.modules.timer = false -- Enable the timer module (boolean) + t.modules.window = false -- Enable the window module (boolean) + t.modules.thread = true -- Enable the thread module (boolean) +end \ No newline at end of file diff --git a/tests/main.lua b/tests/main.lua new file mode 100644 index 0000000..edeee3a --- /dev/null +++ b/tests/main.lua @@ -0,0 +1,10 @@ +package.path = "../?/init.lua;../?.lua;"..package.path + +if os.getenv("LOCAL_LUA_DEBUGGER_VSCODE") == "1" then + require("lldebugger").start() +end + +GLOBAL, THREAD = require("multi.integration.loveManager"):init() + +require("runtests") +require("threadtests") diff --git a/tests/multi b/tests/multi new file mode 120000 index 0000000..b870225 --- /dev/null +++ b/tests/multi @@ -0,0 +1 @@ +../ \ No newline at end of file diff --git a/tests/runtests.lua b/tests/runtests.lua index ae8b098..c5b7c13 100644 --- a/tests/runtests.lua +++ b/tests/runtests.lua @@ -1,185 +1,193 @@ -if os.getenv("LOCAL_LUA_DEBUGGER_VSCODE") == "1" then - package.path="multi/?.lua;multi/?/init.lua;multi/?.lua;multi/?/?/init.lua;"..package.path - require("lldebugger").start() -else - package.path = "../?/init.lua;../?.lua;"..package.path -end ---[[ - This file runs all tests. - Format: - Expected: - ... - ... - ... - Actual: - ... - ... - ... - - Each test that is ran should have a 5 second pause after the test is complete - The expected and actual should "match" (Might be impossible when playing with threads) - This will be pushed directly to the master as tests start existing. -]] -local multi, thread = require("multi"):init{print=true}--{priority=true} -local good = false -local proc = multi:newProcessor("Test",true) -proc:newAlarm(3):OnRing(function() - good = true -end) - -runTest = thread:newFunction(function() - local alarms,tsteps,steps,loops,tloops,updaters,events=false,0,0,0,0,0,false - print("Testing Basic Features. If this fails most other features will probably not work!") - proc:newAlarm(2):OnRing(function(a) - alarms = true - a:Destroy() - end) - proc:newTStep(1,10,1,.1):OnStep(function(t) - tsteps = tsteps + 1 - end):OnEnd(function(step) - step:Destroy() - end) - proc:newStep(1,10):OnStep(function(s) - steps = steps + 1 - end):OnEnd(function(step) - step:Destroy() - end) - local loop = proc:newLoop(function(l) - loops = loops + 1 - end) - proc:newTLoop(function(t) - tloops = tloops + 1 - end,.1) - local updater = proc:newUpdater(1):OnUpdate(function() - updaters = updaters + 1 - end) - local event = proc:newEvent(function() - return alarms - end) - event.OnEvent(function(evnt) - evnt:Destroy() - events = true - print("Alarms: Ok") - print("Events: Ok") - if tsteps == 10 then print("TSteps: Ok") else print("TSteps: Bad!") end - if steps == 10 then print("Steps: Ok") else print("Steps: Bad!") end - if loops > 100 then print("Loops: Ok") else print("Loops: Bad!") end - if tloops > 10 then print("TLoops: Ok") else print("TLoops: Bad!") end - if updaters > 100 then print("Updaters: Ok") else print("Updaters: Bad!") end - end) - thread.hold(event.OnEvent) - print("Starting Connection and Thread tests!") - func = thread:newFunction(function(count) - print("Starting Status test: ",count) - local a = 0 - while true do - a = a + 1 - thread.sleep(.1) - thread.pushStatus(a,count) - if a == count then break end - end - return "Done", true, math.random(1,10000) - end) - local ret = func(10) - local ret2 = func(15) - local ret3 = func(20) - local s1,s2,s3 = 0,0,0 - ret.OnError(function(...) - print("Func 1:",...) - end) - ret2.OnError(function(...) - print("Func 2:",...) - end) - ret3.OnError(function(...) - print("Func 3:",...) - end) - ret.OnStatus(function(part,whole) - s1 = math.ceil((part/whole)*1000)/10 - end) - ret2.OnStatus(function(part,whole) - s2 = math.ceil((part/whole)*1000)/10 - end) - ret3.OnStatus(function(part,whole) - s3 = math.ceil((part/whole)*1000)/10 - end) - - ret.OnReturn(function(...) - print("Done 1",...) - end) - ret2.OnReturn(function(...) - print("Done 2",...) - end) - ret3.OnReturn(function(...) - print("Done 3",...) - end) - - local err, timeout = thread.hold(ret.OnReturn * ret2.OnReturn * ret3.OnReturn) - - if s1 == 100 and s2 == 100 and s3 == 100 then - print("Threads: All tests Ok") - else - if s1>0 and s2>0 and s3 > 0 then - print("Thread OnStatus: Ok") - else - print("Threads OnStatus or thread.hold(conn) Error!") - end - if timeout then - print("Connection Error!") - else - print("Connection Test 1: Ok") - end - print("Connection holding Error!") - end - - conn1 = proc:newConnection() - conn2 = proc:newConnection() - conn3 = proc:newConnection() - local c1,c2,c3,c4 = false,false,false,false - - local a = conn1(function() - c1 = true - end) - - local b = conn2(function() - c2 = true - end) - - local c = conn3(function() - c3 = true - end) - - local d = conn3(function() - c4 = true - end) - - conn1:Fire() - conn2:Fire() - conn3:Fire() - - if c1 and c2 and c3 and c4 then - print("Connection Test 2: Ok") - else - print("Connection Test 2: Error") - end - c3 = false - c4 = false - d:Destroy() - conn3:Fire() - if c3 and not(c4) then - print("Connection Test 3: Ok") - else - print("Connection Test 3: Error removing connection") - end - os.exit() -- End of tests -end) - -print(runTest().OnError(function(...) - print("Error: Something went wrong with the test!") - print(...) - os.exit(1) -end)) - -print("Pumping proc") -while true do - proc.run() +package.path = "../?/init.lua;../?.lua;./init.lua;./?.lua;"..package.path + +local multi, thread = require("multi"):init{print=true,warn=true,error=true}--{priority=true} +local good = false +local proc = multi:newProcessor("Test") + +proc.Start() + +proc:newAlarm(3):OnRing(function() + good = true +end) + +runTest = thread:newFunction(function() + local alarms,tsteps,steps,loops,tloops,updaters,events=false,0,0,0,0,0,false + multi.print("Testing Basic Features. If this fails most other features will probably not work!") + proc:newAlarm(2):OnRing(function(a) + alarms = true + a:Destroy() + end) + proc:newTStep(1,10,1,.1):OnStep(function(t) + tsteps = tsteps + 1 + end):OnEnd(function(step) + step:Destroy() + end) + proc:newStep(1,10):OnStep(function(s) + steps = steps + 1 + end):OnEnd(function(step) + step:Destroy() + end) + local loop = proc:newLoop(function(l) + loops = loops + 1 + end) + proc:newTLoop(function(t) + tloops = tloops + 1 + end,.1) + local updater = proc:newUpdater(1):OnUpdate(function() + updaters = updaters + 1 + end) + local event = proc:newEvent(function() + return alarms + end) + event.OnEvent(function(evnt) + evnt:Destroy() + events = true + multi.success("Alarms: Ok") + multi.success("Events: Ok") + if tsteps == 10 then multi.success("TSteps: Ok") else multi.error("TSteps: Bad!") end + if steps == 10 then multi.success("Steps: Ok") else multi.error("Steps: Bad!") end + if loops > 100 then multi.success("Loops: Ok") else multi.error("Loops: Bad!") end + if tloops > 10 then multi.success("TLoops: Ok") else multi.error("TLoops: Bad!") end + if updaters > 100 then multi.success("Updaters: Ok") else multi.error("Updaters: Bad!") end + end) + thread.hold(event.OnEvent) + multi.print("Starting Connection and Thread tests!") + func = thread:newFunction(function(count) + multi.print("Starting Status test: ",count) + local a = 0 + while true do + a = a + 1 + thread.sleep(.1) + thread.pushStatus(a,count) + if a == count then break end + end + return "Done", true, math.random(1,10000) + end) + local ret = func(10) + local ret2 = func(15) + local ret3 = func(20) + local s1,s2,s3 = 0,0,0 + ret.OnError(function(...) + multi.error("Func 1:",...) + end) + ret2.OnError(function(...) + multi.error("Func 2:",...) + end) + ret3.OnError(function(...) + multi.error("Func 3:",...) + end) + ret.OnStatus(function(part,whole) + s1 = math.ceil((part/whole)*1000)/10 + end) + ret2.OnStatus(function(part,whole) + s2 = math.ceil((part/whole)*1000)/10 + end) + ret3.OnStatus(function(part,whole) + s3 = math.ceil((part/whole)*1000)/10 + end) + + ret.OnReturn(function(...) + multi.success("Done 1",...) + end) + ret2.OnReturn(function(...) + multi.success("Done 2",...) + end) + ret3.OnReturn(function(...) + multi.success("Done 3",...) + end) + + local err, timeout = thread.hold(ret.OnReturn * ret2.OnReturn * ret3.OnReturn) + + if s1 == 100 and s2 == 100 and s3 == 100 then + multi.success("Threads: All tests Ok") + else + if s1>0 and s2>0 and s3 > 0 then + multi.success("Thread OnStatus: Ok") + else + multi.error("Threads OnStatus or thread.hold(conn) Error!") + end + if timeout then + multi.error("Connection Error!") + else + multi.success("Connection Test 1: Ok") + end + multi.error("Connection holding Error!") + end + + conn1 = proc:newConnection() + conn2 = proc:newConnection() + conn3 = proc:newConnection() + local c1,c2,c3,c4 = false,false,false,false + + local a = conn1(function() + c1 = true + end) + + local b = conn2(function() + c2 = true + end) + + local c = conn3(function() + c3 = true + end) + + local d = conn3(function() + c4 = true + end) + + conn1:Fire() + conn2:Fire() + conn3:Fire() + + if c1 and c2 and c3 and c4 then + multi.success("Connection Test 2: Ok") + else + multi.error("Connection Test 2: Error") + end + c3 = false + c4 = false + conn3:Unconnect(d) + conn3:Fire() + if c3 and not(c4) then + multi.success("Connection Test 3: Ok") + else + multi.error("Connection Test 3: Error removing connection") + end + if not love then + local ec = 0 + multi.print("Testing pseudo threading") + capture = io.popen("lua tests/threadtests.lua p"):read("*a") + if capture:lower():match("error") then + ec = ec + 1 + os.exit(1) + else + io.write(capture) + end + multi.print("Testing lanes threading") + capture = io.popen("lua tests/threadtests.lua l"):read("*a") + if capture:lower():match("error") then + ec = ec + 1 + os.exit(1) + else + io.write(capture) + end + os.exit(0) + end +end) + +local handle = runTest() + +handle.OnError(function(...) + multi.error("Something went wrong with the test!") + print(...) +end) + +if not love then + multi:mainloop() +else + local hold = thread:newFunction(function() + thread.hold(handle.OnError + handle.OnReturn) + end, true) + hold() + multi.print("Starting Threading tests!") end \ No newline at end of file diff --git a/tests/test.lua b/tests/test.lua index 4387b11..d5056db 100644 --- a/tests/test.lua +++ b/tests/test.lua @@ -1,133 +1,298 @@ -package.path = "../?/init.lua;../?.lua;"..package.path -multi, thread = require("multi"):init{print=true,findopt=true} -GLOBAL, THREAD = require("multi.integration.lanesManager"):init() -multi:getOptimizationConnection()(function(msg) - print(msg) -end) - --- local conn1, conn2, conn3 = multi:newConnection(), multi:newConnection():fastMode(), multi:newConnection() - --- local link = conn1(function() --- print("Conn1, first") --- end) - --- local link2 = conn1(function() --- print("Conn1, second") --- end) - --- local link3 = conn1(function() --- print("Conn1, third") --- end) - --- local link4 = conn2(function() --- print("Conn2, first") --- end) - --- local link5 = conn2(function() --- print("Conn2, second") --- end) - --- local link6 = conn2(function() --- print("Conn2, third") --- end) - --- print("All conns\n-------------") --- conn1:Fire() --- conn2:Fire() - --- conn1:Unconnect(link3) --- conn2:Unconnect(link6) --- print("All conns Edit\n---------------------") --- conn1:Fire() --- conn2:Fire() - --- thread:newThread(function() --- print("Awaiting status") --- thread.hold(conn1 + (conn2 * conn3)) --- print("Conn or Conn2 and Conn3") --- end) - --- multi:newAlarm(1):OnRing(function() --- print("Conn") --- conn1:Fire() --- end) --- multi:newAlarm(2):OnRing(function() --- print("Conn2") --- conn2:Fire() --- end) --- multi:newAlarm(3):OnRing(function() --- print("Conn3") --- conn3:Fire() --- end) - -local conn = multi:newSystemThreadedConnection("conn"):init() - -multi:newSystemThread("Thread_Test_1", function() - local multi, thread = require("multi"):init() - local conn = GLOBAL["conn"]:init() - local console = THREAD.getConsole() - conn(function(a,b,c) - console.print(THREAD:getName().." was triggered!",a,b,c) - end) - multi:mainloop() -end) - -multi:newSystemThread("Thread_Test_2", function() - local multi, thread = require("multi"):init() - local conn = GLOBAL["conn"]:init() - local console = THREAD.getConsole() - conn(function(a,b,c) - console.print(THREAD:getName().." was triggered!",a,b,c) - end) - multi:newAlarm(2):OnRing(function() - console.print("Fire 2!!!") - conn:Fire(4,5,6) - THREAD.kill() - end) - - multi:mainloop() -end) -local console = THREAD.getConsole() -conn(function(a,b,c) - console.print("Mainloop conn got triggered!",a,b,c) -end) - -alarm = multi:newAlarm(1) -alarm:OnRing(function() - console.print("Fire 1!!!") - conn:Fire(1,2,3) -end) - -alarm = multi:newAlarm(3):OnRing(function() - multi:newSystemThread("Thread_Test_3",function() - local multi, thread = require("multi"):init() - local conn = GLOBAL["conn"]:init() - local console = THREAD.getConsole() - conn(function(a,b,c) - console.print(THREAD:getName().." was triggered!",a,b,c) - end) - multi:newAlarm(4):OnRing(function() - console.print("Fire 3!!!") - conn:Fire(7,8,9) - end) - multi:mainloop() - end) -end) - -multi:newSystemThread("Thread_Test_4",function() - local multi, thread = require("multi"):init() - local conn = GLOBAL["conn"]:init() - local conn2 = multi:newConnection() - local console = THREAD.getConsole() - multi:newAlarm(2):OnRing(function() - conn2:Fire() - end) - multi:newThread(function() - console.print("Conn Test!") - thread.hold(conn + conn2) - console.print("It held!") - end) - multi:mainloop() -end) - -multi:mainloop() \ No newline at end of file +package.path = "../?/init.lua;../?.lua;"..package.path +multi, thread = require("multi"):init{print=true,warn=true,debugging=true} +-- for i,v in pairs(thread) do +-- print(i,v) +-- end + +-- require("multi.integration.priorityManager") + +-- multi.debugging.OnObjectCreated(function(obj, process) +-- multi.print("Created:", obj.Type, "in", process.Type, process:getFullName()) +-- end) + +-- multi.debugging.OnObjectDestroyed(function(obj, process) +-- multi.print("Destroyed:", obj.Type, "in", process.Type, process:getFullName()) +-- end) + + +-- test = multi:newProcessor("Test") +-- test:setPriorityScheme(multi.priorityScheme.TimeBased) + +-- test:newUpdater(10000000):OnUpdate(function() +-- print("Print is slowish") +-- end) + +-- print("Running...") + +-- local conn1, conn2 = multi:newConnection(), multi:newConnection() +-- conn3 = conn1 + conn2 + +-- conn1(function() +-- print("Hi 1") +-- end) + +-- conn2(function() +-- print("Hi 2") +-- end) + +-- conn3(function() +-- print("Hi 3") +-- end) + +-- function test(a,b,c) +-- print("I run before all and control if execution should continue!") +-- return a>b +-- end + +-- conn4 = test .. conn1 + +-- conn5 = conn2 .. function() print("I run after it all!") end + +-- conn4:Fire(3,2,3) +-- -- This second one won't trigger the Hi's +-- conn4:Fire(1,2,3) + +-- conn5(function() +-- print("Test 1") +-- end) + +-- conn5(function() +-- print("Test 2") +-- end) + +-- conn5(function() +-- print("Test 3") +-- end) + +-- conn5:Fire() + + + + +-- multi.print("Testing thread:newProcessor()") + +-- proc = thread:newProcessor("Test") + +-- proc:newLoop(function() +-- multi.print("Running...") +-- thread.sleep(1) +-- end) + +-- proc:newThread(function() +-- while true do +-- multi.warn("Everything is a thread in this proc!") +-- thread.sleep(1) +-- end +-- end) + +-- proc:newAlarm(5):OnRing(function(a) +-- multi.print(";) Goodbye") +-- a:Destroy() +-- end) + +-- local func = thread:newFunction(function() +-- thread.sleep(4) +-- print("Hello!") +-- end) + +-- multi:newTLoop(func, 1) + +-- multi:mainloop() + +-- multi:setTaskDelay(.05) +-- multi:newTask(function() +-- for i = 1, 10 do +-- multi:newTask(function() +-- print("Task "..i) +-- end) +-- end +-- end) + +-- local conn = multi:newConnection() +-- conn(function() print("Test 1") end) +-- conn(function() print("Test 2") end) +-- conn(function() print("Test 3") end) +-- conn(function() print("Test 4") end) + +-- print("Fire 1") +-- conn:Fire() +-- conn = -conn +-- print("Fire 2") +-- conn:Fire() + +-- print(#conn) + +-- thread:newThread("Test thread", function() +-- print("Starting thread!") +-- thread.defer(function() -- Runs when the thread finishes execution +-- print("Clean up time!") +-- end) +-- --[[ +-- Do lot's of stuff +-- ]] +-- thread.sleep(3) +-- end) + +multi:mainloop() + +-- local conn1, conn2, conn3 = multi:newConnection(nil,nil,true), multi:newConnection(), multi:newConnection() + +-- local link = conn1(function() +-- print("Conn1, first") +-- end) + +-- local link2 = conn1(function() +-- print("Conn1, second") +-- end) + +-- local link3 = conn1(function() +-- print("Conn1, third") +-- end) + +-- local link4 = conn2(function() +-- print("Conn2, first") +-- end) + +-- local link5 = conn2(function() +-- print("Conn2, second") +-- end) + +-- local link6 = conn2(function() +-- print("Conn2, third") +-- end) + +-- print("Links 1-6",link,link2,link3,link4,link5,link6) +-- conn1:Lock(link) +-- print("All conns\n-------------") +-- conn1:Fire() +-- conn2:Fire() + +-- conn1:Unlock(link) + +-- conn1:Unconnect(link3) +-- conn2:Unconnect(link6) +-- print("All conns Edit\n---------------------") +-- conn1:Fire() +-- conn2:Fire() + +-- thread:newThread(function() +-- print("Awaiting status") +-- thread.hold(conn1 + (conn2 * conn3)) +-- print("Conn or Conn2 and Conn3") +-- end) + +-- multi:newAlarm(1):OnRing(function() +-- print("Conn") +-- conn1:Fire() +-- end) +-- multi:newAlarm(2):OnRing(function() +-- print("Conn2") +-- conn2:Fire() +-- end) +-- multi:newAlarm(3):OnRing(function() +-- print("Conn3") +-- conn3:Fire() +-- os.exit() +-- end) + + +-- local conn = multi:newSystemThreadedConnection("conn"):init() + +-- multi:newSystemThread("Thread_Test_1", function() +-- local multi, thread = require("multi"):init() +-- local conn = GLOBAL["conn"]:init() +-- local console = THREAD.getConsole() +-- conn(function(a,b,c) +-- console.print(THREAD:getName().." was triggered!",a,b,c) +-- end) +-- multi:mainloop() +-- end) + +-- multi:newSystemThread("Thread_Test_2", function() +-- local multi, thread = require("multi"):init() +-- local conn = GLOBAL["conn"]:init() +-- local console = THREAD.getConsole() +-- conn(function(a,b,c) +-- console.print(THREAD:getName().." was triggered!",a,b,c) +-- end) +-- multi:newAlarm(2):OnRing(function() +-- console.print("Fire 2!!!") +-- conn:Fire(4,5,6) +-- THREAD.kill() +-- end) + +-- multi:mainloop() +-- end) +-- local console = THREAD.getConsole() +-- conn(function(a,b,c) +-- console.print("Mainloop conn got triggered!",a,b,c) +-- end) + +-- alarm = multi:newAlarm(1) +-- alarm:OnRing(function() +-- console.print("Fire 1!!!") +-- conn:Fire(1,2,3) +-- end) + +-- alarm = multi:newAlarm(3):OnRing(function() +-- multi:newSystemThread("Thread_Test_3",function() +-- local multi, thread = require("multi"):init() +-- local conn = GLOBAL["conn"]:init() +-- local console = THREAD.getConsole() +-- conn(function(a,b,c) +-- console.print(THREAD:getName().." was triggered!",a,b,c) +-- end) +-- multi:newAlarm(4):OnRing(function() +-- console.print("Fire 3!!!") +-- conn:Fire(7,8,9) +-- end) +-- multi:mainloop() +-- end) +-- end) + +-- multi:newSystemThread("Thread_Test_4",function() +-- local multi, thread = require("multi"):init() +-- local conn = GLOBAL["conn"]:init() +-- local conn2 = multi:newConnection() +-- local console = THREAD.getConsole() +-- multi:newAlarm(2):OnRing(function() +-- conn2:Fire() +-- end) +-- multi:newThread(function() +-- console.print("Conn Test!") +-- thread.hold(conn + conn2) +-- console.print("It held!") +-- end) +-- multi:mainloop() +-- end) + +-- multi:mainloop() +--[[ + newFunction function: 0x00fad170 + waitFor function: 0x00fad0c8 + request function: 0x00fa4f10 + newThread function: 0x00fad1b8 + --__threads table: 0x00fa4dc8 + defer function: 0x00fa4f98 + isThread function: 0x00facd40 + holdFor function: 0x00fa5058 + yield function: 0x00faccf8 + hold function: 0x00fa51a0 + chain function: 0x00fa5180 + __CORES 32 + newISOThread function: 0x00fad250 + newFunctionBase function: 0x00fad128 + requests table: 0x00fa4e68 + newProcessor function: 0x00fad190 + exec function: 0x00fa50e8 + pushStatus function: 0x00fad108 + kill function: 0x00faccd8 + get function: 0x00fad0a8 + set function: 0x00fad088 + getCores function: 0x00facd60 + skip function: 0x00faccb0 + --_Requests function: 0x00fa50a0 + getRunningThread function: 0x00fa4fb8 + holdWithin function: 0x00facc80 + sleep function: 0x00fa4df0 +]] \ No newline at end of file diff --git a/tests/threadtests.lua b/tests/threadtests.lua new file mode 100644 index 0000000..9570a39 --- /dev/null +++ b/tests/threadtests.lua @@ -0,0 +1,246 @@ +package.path = "D:/VSCWorkspace/?/init.lua;D:/VSCWorkspace/?.lua;"..package.path +package.cpath = "C:/luaInstalls/lua5.4/lib/lua/5.4/?/core.dll;" .. package.cpath +multi, thread = require("multi"):init{error=true,warning=true,print=true, priority=true} +proc = multi:newProcessor("Thread Test",true) +local LANES, LOVE, PSEUDO = 1, 2, 3 +local env, we_good + +if love then + GLOBAL, THREAD = require("multi.integration.loveManager"):init() + env = LOVE +elseif arg[1] == "l" then + GLOBAL, THREAD = require("multi.integration.lanesManager"):init() + env = LANES +elseif arg[1] == "p" then + GLOBAL, THREAD = require("multi.integration.pseudoManager"):init() + env = PSEUDO +else + io.write("Test Pseudo(p), Lanes(l), or love(Run in love environment) Threading: ") + choice = io.read() + if choice == "p" then + GLOBAL, THREAD = require("multi.integration.pseudoManager"):init() + env = PSEUDO + elseif choice == "l" then + GLOBAL, THREAD = require("multi.integration.lanesManager"):init() + env = LANES + else + error("Invalid threading choice") + end +end + +multi.print("Testing THREAD.setENV() if the multi_assert is not found then there is a problem") +THREAD.setENV({ + multi_assert = function(expected, actual, s) + if expected ~= actual then + multi.error(s .. " Expected: '".. tostring(expected) .."' Actual: '".. tostring(actual) .."'") + end + end +}) + +multi:newThread("Scheduler Thread",function() + multi:newThread(function() + thread.sleep(30) + print("Timeout tests took longer than 30 seconds") + multi:Stop() + os.exit(1) + end) + + queue = multi:newSystemThreadedQueue("Test_Queue") + defer_queue = multi:newSystemThreadedQueue("Defer_Queue") + + multi:newSystemThread("Test_Thread_0", function() + defer_queue = THREAD.waitFor("Defer_Queue"):init() + + THREAD.defer(function() + defer_queue:push("done") + multi.print("This function was defered until the end of the threads life") + end) + + multi.print("Testing defer, should print below this") + + if THREAD_NAME~="Test_Thread_0" then + multi.error("The name should be Test_Thread_0",THREAD_NAME,THREAD_NAME,_G.THREAD_NAME) + end + end) + + if thread.hold(function() + return defer_queue:pop() == "done" + end,{sleep=3}) == nil then + multi.error("Thread.defer didn't work!") + end + + th1 = multi:newSystemThread("Test_Thread_1", function(a,b,c,d,e,f) + queue = THREAD.waitFor("Test_Queue"):init() + multi_assert("Test_Thread_1", THREAD_NAME, "Thread name does not match!") + multi_assert("Passing some args", a, "First argument is not as expected 'Passing some args'") + multi_assert(true, e, "Argument e is not true!") + multi_assert("table", type(f), "Argument f is not a table!") + queue:push("done") + end,"Passing some args", 1, 2, 3, true, {"Table"}).OnError(function(self,err) + multi.error(err) + os.exit(1) + end) + + if thread.hold(function() + return queue:pop() == "done" + end,{sleep=1}) == nil then + thread.kill() + end + + multi.success("Thread Spawning, THREAD namaspace in threads, global's working, and queues for passing data: Ok") + + func = THREAD:newFunction(function(a,b,c) + assert(a == 3, "First argument expected '3' got '".. a .."'!") + assert(b == 2, "Second argument expected '2' got '".. b .."'!") + assert(c == 1, "Third argument expected '1' got '".. c .."'!") + return 1, 2, 3, {"a table"} + end, true) -- Hold this + + a, b, c, d = func(3,2,1) + assert(a == 1, "First return was not '1'!") + assert(b == 2, "Second return was not '2'!") + assert(c == 3, "Third return was not '3'!") + assert(d[1] == "a table", "Fourth return is not table, or doesn't contain 'a table'!") + + multi.success("Threaded Functions, arg passing, return passing, holding: Ok") + + test=multi:newSystemThreadedTable("YO"):init() + test["test1"]="tabletest" + local worked = false + + multi:newSystemThread("testing tables",function() + tab=THREAD.waitFor("YO") + THREAD.hold(function() return tab["test1"] end) + THREAD.sleep(.1) + tab["test2"] = "Whats so funny?" + end).OnError(multi.error) + + multi:newThread("test2",function() + thread.hold(function() return test["test2"] end) + worked = true + end) + + t, val = thread.hold(function() + return worked + end,{sleep=2}) + + if val == multi.TIMEOUT then + multi.error("SystemThreadedTables: Failed") + os.exit(1) + end + + multi.success("SystemThreadedTables: Ok") + + local ready = false + + jq = multi:newSystemThreadedJobQueue(4) -- Job queue with 4 worker threads + func2 = jq:newFunction("sleep",function(a,b) + THREAD.sleep(.2) + end) + func = jq:newFunction("test-thread",function(a,b) + sleep() + return a+b + end) + local count = 0 + for i = 1,10 do + func(i, i*3).OnReturn(function(data) + count = count + 1 + end) + end + + t, val = thread.hold(function() + return count == 10 + end,{sleep=3}) + + if val == multi.TIMEOUT then + multi.error("SystemThreadedJobQueues: Failed") + os.exit(1) + end + + multi.success("SystemThreadedJobQueues: Ok") + + local proxy_test = false + local stp = multi:newSystemThreadedProcessor(5) + + local tloop = stp:newTLoop(function() + --print("Test") + end, 1) + + multi:newSystemThread("PROX_THREAD",function(tloop) + local multi, thread = require("multi"):init() + tloop = tloop:init() + multi.print("tloop type:",tloop.Type) + multi.print("Testing proxies on other threads") + thread:newThread(function() + while true do + thread.hold(tloop.OnLoop) + print(THREAD_NAME,"Loopy") + end + end) + tloop.OnLoop(function(a) + print(THREAD_NAME, "Got loop...") + end) + multi:mainloop() + end, tloop:getTransferable()) + + local test = tloop:getTransferable() + + multi.print("tloop", tloop.Type) + multi.print("tloop.OnLoop", tloop.OnLoop.Type) + + thread:newThread("Proxy Test Thread",function() + multi.print("Testing holding on a proxy connection!") + thread.hold(tloop.OnLoop) + multi.print("Held on proxy connection... once") + thread.hold(tloop.OnLoop) + multi.print("Held on proxy connection... twice") + thread.hold(tloop.OnLoop) + multi.print("Held on proxy connection... finally") + proxy_test = true + end).OnError(print) + + thread:newThread(function() + thread.defer(function() + multi.print("Something happened!") + end) + while true do + thread.hold(tloop.OnLoop) + multi.print(THREAD_NAME,"Local Loopy") + end + end).OnError(function(...) + print("Error",...) + end) + + tloop.OnLoop(function() + print("OnLoop", THREAD_NAME) + end) + + t, val = thread.hold(function() + return proxy_test + end,{sleep=10}) + + if val == multi.TIMEOUT then + multi.error("SystemThreadedProcessor/Proxies: Failed") + os.exit(1) + else + multi.success("SystemThreadedProcessor: OK") + end + + thread.sleep(2) + + we_good = true + multi:Stop() -- Needed in love2d tests to stop the main runner + os.exit(0) +end) + +multi.OnExit(function(err_or_errorcode) + multi.print("EC: ", err_or_errorcode) + if not we_good then + multi.print("There was an error running some tests!") + return + else + multi.success("Tests complete!") + end +end) + +multi:mainloop() \ No newline at end of file diff --git a/tests/vscode-debuggee.lua b/tests/vscode-debuggee.lua new file mode 100644 index 0000000..61b47fb --- /dev/null +++ b/tests/vscode-debuggee.lua @@ -0,0 +1,1102 @@ +local debuggee = {} + +local socket = require 'socket.core' +local json +local handlers = {} +local sock +local directorySeperator = package.config:sub(1,1) +local sourceBasePath = '.' +local storedVariables = {} +local nextVarRef = 1 +local baseDepth +local breaker +local sendEvent +local dumpCommunication = false +local ignoreFirstFrameInC = false +local debugTargetCo = nil +local redirectedPrintFunction = nil + +local onError = nil +local addUserdataVar = nil + +local function defaultOnError(e) + print('****************************************************') + print(e) + print('****************************************************') +end + +local function valueToString(value, depth) + local str = '' + depth = depth or 0 + local t = type(value) + if t == 'table' then + str = str .. '{\n' + for k, v in pairs(value) do + str = str .. string.rep(' ', depth + 1) .. '[' .. valueToString(k) ..']' .. ' = ' .. valueToString(v, depth + 1) .. ',\n' + end + str = str .. string.rep(' ', depth) .. '}' + elseif t == 'string' then + str = str .. '"' .. tostring(value) .. '"' + else + str = str .. tostring(value) + end + return str +end + +------------------------------------------------------------------------------- +local sethook = debug.sethook +debug.sethook = nil + +local cocreate = coroutine.create +coroutine.create = function(f) + local c = cocreate(f) + debuggee.addCoroutine(c) + return c +end + +------------------------------------------------------------------------------- +local function debug_getinfo(depth, what) + if debugTargetCo then + return debug.getinfo(debugTargetCo, depth, what) + else + return debug.getinfo(depth + 1, what) + end +end + +------------------------------------------------------------------------------- +local function debug_getlocal(depth, i) + if debugTargetCo then + return debug.getlocal(debugTargetCo, depth, i) + else + return debug.getlocal(depth + 1, i) + end +end + +------------------------------------------------------------------------------- +local DO_TEST = false + +------------------------------------------------------------------------------- +-- chunkname matching {{{ +local function getMatchCount(a, b) + local n = math.min(#a, #b) + for i = 0, n - 1 do + if a[#a - i] == b[#b - i] then + -- pass + else + return i + end + end + return n +end +if DO_TEST then + assert(getMatchCount({'a','b','c'}, {'a','b','c'}) == 3) + assert(getMatchCount({'b','c'}, {'a','b','c'}) == 2) + assert(getMatchCount({'a','b','c'}, {'b','c'}) == 2) + assert(getMatchCount({}, {'a','b','c'}) == 0) + assert(getMatchCount({'a','b','c'}, {}) == 0) + assert(getMatchCount({'a','b','c'}, {'a','b','c','d'}) == 0) +end + +local function splitChunkName(s) + if string.sub(s, 1, 1) == '@' then + s = string.sub(s, 2) + end + + local a = {} + for word in string.gmatch(s, '[^/\\]+') do + a[#a + 1] = string.lower(word) + end + return a +end +if DO_TEST then + local a = splitChunkName('@.\\vscode-debuggee.lua') + assert(#a == 2) + assert(a[1] == '.') + assert(a[2] == 'vscode-debuggee.lua') + + local a = splitChunkName('@C:\\dev\\VSCodeLuaDebug\\debuggee/lua\\socket.lua') + assert(#a == 6) + assert(a[1] == 'c:') + assert(a[2] == 'dev') + assert(a[3] == 'vscodeluadebug') + assert(a[4] == 'debuggee') + assert(a[5] == 'lua') + assert(a[6] == 'socket.lua') + + local a = splitChunkName('@main.lua') + assert(#a == 1) + assert(a[1] == 'main.lua') +end +-- chunkname matching }}} + +-- path control {{{ +local Path = {} + +function Path.isAbsolute(a) + local firstChar = string.sub(a, 1, 1) + if firstChar == '/' or firstChar == '\\' then + return true + end + + if string.match(a, '^%a%:[/\\]') then + return true + end + + return false +end + +local np_pat1, np_pat2 = ('[^SEP:]+SEP%.%.SEP?'):gsub('SEP', directorySeperator), ('SEP+%.?SEP'):gsub('SEP', directorySeperator) +function Path.normpath(path) + path = path:gsub('[/\\]', directorySeperator) + + if directorySeperator == '\\' then + local unc = ('SEPSEP'):gsub('SEP', directorySeperator) -- UNC + if path:match('^'..unc) then + return unc..Path.normpath(path:sub(3)) + end + end + + local k + repeat -- /./ -> / + path,k = path:gsub(np_pat2, directorySeperator) + until k == 0 + repeat -- A/../ -> (empty) + path,k = path:gsub(np_pat1, '', 1) + until k == 0 + if path == '' then + path = '.' + end + return path +end + +function Path.concat(a, b) + -- normalize a + local lastChar = string.sub(a, #a, #a) + if not (lastChar == '/' or lastChar == '\\') then + a = a .. directorySeperator + end + + -- normalize b + if string.match(b, '^%.%\\') or string.match(b, '^%.%/') then + b = string.sub(b, 3) + end + + return a .. b +end + +function Path.toAbsolute(base, sub) + if Path.isAbsolute(sub) then + return Path.normpath(sub) + else + return Path.normpath(Path.concat(base, sub)) + end +end + +if DO_TEST then + assert(Path.isAbsolute('c:\\asdf\\afsd')) + assert(Path.isAbsolute('c:/asdf/afsd')) + if directorySeperator == '\\' then + assert(Path.toAbsolute('c:\\asdf', 'fdsf') == 'c:\\asdf\\fdsf') + assert(Path.toAbsolute('c:\\asdf', '.\\fdsf') == 'c:\\asdf\\fdsf') + assert(Path.toAbsolute('c:\\asdf', '..\\fdsf') == 'c:\\fdsf') + assert(Path.toAbsolute('c:\\asdf', 'c:\\fdsf') == 'c:\\fdsf') + assert(Path.toAbsolute('c:/asdf', '../fdsf') == 'c:\\fdsf') + assert(Path.toAbsolute('\\\\HOST\\asdf', '..\\fdsf') == '\\\\HOST\\fdsf') + elseif directorySeperator == '/' then + assert(Path.toAbsolute('/usr/bin/asdf', 'fdsf') == '/usr/bin/asdf/fdsf') + assert(Path.toAbsolute('/usr/bin/asdf', './fdsf') == '/usr/bin/asdf/fdsf') + assert(Path.toAbsolute('/usr/bin/asdf', '../fdsf') == '/usr/bin/fdsf') + assert(Path.toAbsolute('/usr/bin/asdf', '/usr/bin/fdsf') == '/usr/bin/fdsf') + assert(Path.toAbsolute('\\usr\\bin\\asdf', '..\\fdsf') == '/usr/bin/fdsf') + end +end +-- path control }}} + +local coroutineSet = {} +setmetatable(coroutineSet, { __mode = 'v' }) + +------------------------------------------------------------------------------- +-- network utility {{{ +local function sendFully(str) + local first = 1 + while first <= #str do + local sent = sock:send(str, first) + if sent and sent > 0 then + first = first + sent; + else + error('sock:send() returned < 0') + end + end +end + +-- send log to debug console +local function logToDebugConsole(output, category) + local dumpMsg = { + event = 'output', + type = 'event', + body = { + category = category or 'console', + output = output + } + } + local dumpBody = json.encode(dumpMsg) + sendFully('#' .. #dumpBody .. '\n' .. dumpBody) +end + +-- pure mode {{{ +local function createHaltBreaker() + -- chunkname matching { + local loadedChunkNameMap = {} + for chunkname, _ in pairs(debug.getchunknames()) do + loadedChunkNameMap[chunkname] = splitChunkName(chunkname) + end + + local function findMostSimilarChunkName(path) + local splitedReqPath = splitChunkName(path) + local maxMatchCount = 0 + local foundChunkName = nil + for chunkName, splitted in pairs(loadedChunkNameMap) do + local count = getMatchCount(splitedReqPath, splitted) + if (count > maxMatchCount) then + maxMatchCount = count + foundChunkName = chunkName + end + end + return foundChunkName + end + -- chunkname matching } + + local lineBreakCallback = nil + local function updateCoroutineHook(c) + if lineBreakCallback then + sethook(c, lineBreakCallback, 'l') + else + sethook(c) + end + end + local function sethalt(cname, ln) + for i = ln, ln + 10 do + if debug.sethalt(cname, i) then + return i + end + end + return nil + end + return { + setBreakpoints = function(path, lines) + local foundChunkName = findMostSimilarChunkName(path) + local verifiedLines = {} + + if foundChunkName then + debug.clearhalt(foundChunkName) + for _, ln in ipairs(lines) do + verifiedLines[ln] = sethalt(foundChunkName, ln) + end + end + + return verifiedLines + end, + + setLineBreak = function(callback) + if callback then + sethook(callback, 'l') + else + sethook() + end + + lineBreakCallback = callback + for cid, c in pairs(coroutineSet) do + updateCoroutineHook(c) + end + end, + + coroutineAdded = function(c) + updateCoroutineHook(c) + end, + + stackOffset = + { + enterDebugLoop = 6, + halt = 6, + step = 4, + stepDebugLoop = 6 + } + } +end + +local function createPureBreaker() + local lineBreakCallback = nil + local breakpointsPerPath = {} + local chunknameToPathCache = {} + + local function chunkNameToPath(chunkname) + local cached = chunknameToPathCache[chunkname] + if cached then + return cached + end + + local splitedReqPath = splitChunkName(chunkname) + local maxMatchCount = 0 + local foundPath = nil + for path, _ in pairs(breakpointsPerPath) do + local splitted = splitChunkName(path) + local count = getMatchCount(splitedReqPath, splitted) + if (count > maxMatchCount) then + maxMatchCount = count + foundPath = path + end + end + + if foundPath then + chunknameToPathCache[chunkname] = foundPath + end + return foundPath + end + + local entered = false + local function hookfunc() + if entered then return false end + entered = true + + if lineBreakCallback then + lineBreakCallback() + end + + local info = debug_getinfo(2, 'Sl') + if info then + local path = chunkNameToPath(info.source) + if path then + path = string.lower(path) + end + local bpSet = breakpointsPerPath[path] + if bpSet and bpSet[info.currentline] then + _G.__halt__() + end + end + + entered = false + end + sethook(hookfunc, 'l') + + return { + setBreakpoints = function(path, lines) + local t = {} + local verifiedLines = {} + for _, ln in ipairs(lines) do + t[ln] = true + verifiedLines[ln] = ln + end + if path then + path = string.lower(path) + end + breakpointsPerPath[path] = t + return verifiedLines + end, + + setLineBreak = function(callback) + lineBreakCallback = callback + end, + + coroutineAdded = function(c) + sethook(c, hookfunc, 'l') + end, + + stackOffset = + { + enterDebugLoop = 6, + halt = 7, + step = 4, + stepDebugLoop = 7 + } + } +end +-- pure mode }}} + + +-- 센드는 블럭이어도 됨. +local function sendMessage(msg) + local body = json.encode(msg) + + if dumpCommunication then + logToDebugConsole('[SENDING] ' .. valueToString(msg)) + end + + sendFully('#' .. #body .. '\n' .. body) +end + +-- 리시브는 블럭이 아니어야 할 거 같은데... 음... 블럭이어도 괜찮나? +local function recvMessage() + local header = sock:receive('*l') + if (header == nil) then + -- 디버거가 떨어진 상황 + return nil + end + if (string.sub(header, 1, 1) ~= '#') then + error('헤더 이상함:' .. header) + end + + local bodySize = tonumber(header:sub(2)) + local body = sock:receive(bodySize) + + return json.decode(body) +end +-- network utility }}} + +------------------------------------------------------------------------------- +local function debugLoop() + storedVariables = {} + nextVarRef = 1 + while true do + local msg = recvMessage() + if msg then + if dumpCommunication then + logToDebugConsole('[RECEIVED] ' .. valueToString(msg), 'stderr') + end + + local fn = handlers[msg.command] + if fn then + local rv = fn(msg) + + -- continue인데 break하는 게 역설적으로 느껴지지만 + -- 디버그 루프를 탈출(break)해야 정상 실행 흐름을 계속(continue)할 수 있지.. + if (rv == 'CONTINUE') then + break; + end + else + --print('UNKNOWN DEBUG COMMAND: ' .. tostring(msg.command)) + end + else + -- 디버그 중에 디버거가 떨어졌다. + -- print펑션을 리다이렉트 한경우에는 원래대로 돌려놓는다 + if redirectedPrintFunction then + _G.print = redirectedPrintFunction + end + break + end + end + storedVariables = {} + nextVarRef = 1 +end + +------------------------------------------------------------------------------- +local sockArray = {} +function debuggee.start(jsonLib, config) + json = jsonLib + assert(jsonLib) + + config = config or {} + local connectTimeout = config.connectTimeout or 5.0 + local controllerHost = config.controllerHost or 'localhost' + local controllerPort = config.controllerPort or 56789 + onError = config.onError or defaultOnError + addUserdataVar = config.addUserdataVar or function() return end + local redirectPrint = config.redirectPrint or false + dumpCommunication = config.dumpCommunication or false + ignoreFirstFrameInC = config.ignoreFirstFrameInC or false + if not config.luaStyleLog then + valueToString = function(value) return json.encode(value) end + end + + local breakerType + if debug.sethalt then + breaker = createHaltBreaker() + breakerType = 'halt' + else + breaker = createPureBreaker() + breakerType = 'pure' + end + + local err + sock, err = socket.tcp() + if not sock then error(err) end + sockArray = { sock } + if sock.settimeout then sock:settimeout(connectTimeout) end + local res, err = sock:connect(controllerHost, tostring(controllerPort)) + if not res then + sock:close() + sock = nil + return false, breakerType + end + + if sock.settimeout then sock:settimeout() end + sock:setoption('tcp-nodelay', true) + + local initMessage = recvMessage() + assert(initMessage and initMessage.command == 'welcome') + sourceBasePath = initMessage.sourceBasePath + directorySeperator = initMessage.directorySeperator + + if redirectPrint then + redirectedPrintFunction = _G.print -- 디버거가 떨어질때를 대비해서 보관한다 + _G.print = function(...) + local t = { n = select("#", ...), ... } + for i = 1, #t do + t[i] = tostring(t[i]) + end + sendEvent( + 'output', + { + category = 'stdout', + output = table.concat(t, '\t') .. '\n' -- Same as default "print" output end new line. + }) + end + end + + debugLoop() + return true, breakerType +end + +------------------------------------------------------------------------------- +function debuggee.poll() + if not sock then return end + + -- Processes commands in the queue. + -- Immediately returns when the queue is/became empty. + while true do + local r, w, e = socket.select(sockArray, nil, 0) + if e == 'timeout' then break end + + local msg = recvMessage() + if msg then + if dumpCommunication then + logToDebugConsole('[POLL-RECEIVED] ' .. valueToString(msg), 'stderr') + end + + if msg.command == 'pause' then + debuggee.enterDebugLoop(1) + return + end + + local fn = handlers[msg.command] + if fn then + local rv = fn(msg) + -- Ignores rv, because this loop never blocks except explicit pause command. + else + --print('POLL-UNKNOWN DEBUG COMMAND: ' .. tostring(msg.command)) + end + else + break + end + end +end + +------------------------------------------------------------------------------- +local function getCoroutineId(c) + -- 'thread: 011DD5B0' + -- 12345678^ + local threadIdHex = string.sub(tostring(c), 9) + return tonumber(threadIdHex, 16) +end + +------------------------------------------------------------------------------- +function debuggee.addCoroutine(c) + local cid = getCoroutineId(c) + coroutineSet[cid] = c + breaker.coroutineAdded(c) +end + +------------------------------------------------------------------------------- +local function sendSuccess(req, body) + sendMessage({ + command = req.command, + success = true, + request_seq = req.seq, + type = "response", + body = body + }) +end + +------------------------------------------------------------------------------- +local function sendFailure(req, msg) + sendMessage({ + command = req.command, + success = false, + request_seq = req.seq, + type = "response", + message = msg + }) +end + +------------------------------------------------------------------------------- +sendEvent = function(eventName, body) + sendMessage({ + event = eventName, + type = "event", + body = body + }) +end + +------------------------------------------------------------------------------- +local function currentThreadId() +--[[ + local threadId = 0 + if coroutine.running() then + end + return threadId +]] + return 0 +end + +------------------------------------------------------------------------------- +local function startDebugLoop() + sendEvent( + 'stopped', + { + reason = 'breakpoint', + threadId = currentThreadId(), + allThreadsStopped = true + }) + + local status, err = pcall(debugLoop) + if not status then + onError(err) + end +end + +------------------------------------------------------------------------------- +_G.__halt__ = function() + baseDepth = breaker.stackOffset.halt + startDebugLoop() +end + +------------------------------------------------------------------------------- +function debuggee.enterDebugLoop(depthOrCo, what) + if sock == nil then + return false + end + + if what then + sendEvent( + 'output', + { + category = 'stderr', + output = what, + }) + end + + if type(depthOrCo) == 'thread' then + baseDepth = 0 + debugTargetCo = depthOrCo + elseif type(depthOrCo) == 'table' then + baseDepth = (depthOrCo.depth or 0) + debugTargetCo = depthOrCo.co + else + baseDepth = (depthOrCo or 0) + breaker.stackOffset.enterDebugLoop + debugTargetCo = nil + end + startDebugLoop() + return true +end + +------------------------------------------------------------------------------- +-- Function for printing on vscode debug console +-- First parameter 'category' can colorizes print text +function debuggee.print(category, ...) + if sock == nil then + return false + end + local t = { ... } + for i = 1, #t do + t[i] = tostring(t[i]) + end + + local categoryVscodeConsole = 'stdout' + if category == 'warning' then + categoryVscodeConsole = 'console' -- yellow + elseif category == 'error' then + categoryVscodeConsole = 'stderr' -- red + elseif category == 'log' then + categoryVscodeConsole = 'stdout' -- white + end + + sendEvent( + 'output', + { + category = categoryVscodeConsole, + output = table.concat(t, '\t') .. '\n' -- Same as default "print" output end new line. + }) +end + +------------------------------------------------------------------------------- +-- ★★★ https://github.com/Microsoft/vscode-debugadapter-node/blob/master/protocol/src/debugProtocol.ts +------------------------------------------------------------------------------- + +------------------------------------------------------------------------------- +function handlers.setBreakpoints(req) + local bpLines = {} + for _, bp in ipairs(req.arguments.breakpoints) do + bpLines[#bpLines + 1] = bp.line + end + + local verifiedLines = breaker.setBreakpoints( + req.arguments.source.path, + bpLines) + + local breakpoints = {} + for i, ln in ipairs(bpLines) do + breakpoints[i] = { + verified = (verifiedLines[ln] ~= nil), + line = verifiedLines[ln] + } + end + + sendSuccess(req, { + breakpoints = breakpoints + }) +end + +------------------------------------------------------------------------------- +function handlers.configurationDone(req) + sendSuccess(req, {}) + return 'CONTINUE' +end + +------------------------------------------------------------------------------- +function handlers.threads(req) + local c = coroutine.running() + + local mainThread = { + id = currentThreadId(), + name = (c and tostring(c)) or "main" + } + + sendSuccess(req, { + threads = { mainThread } + }) +end + +------------------------------------------------------------------------------- +function handlers.stackTrace(req) + assert(req.arguments.threadId == 0) + + local stackFrames = {} + local firstFrame = (req.arguments.startFrame or 0) + baseDepth + local lastFrame = (req.arguments.levels and (req.arguments.levels ~= 0)) + and (firstFrame + req.arguments.levels - 1) + or (9999) + + -- if firstframe function of stack is C function, ignore it. + if ignoreFirstFrameInC then + local info = debug_getinfo(firstFrame, 'lnS') + if info and info.what == "C" then + firstFrame = firstFrame + 1 + end + end + + for i = firstFrame, lastFrame do + local info = debug_getinfo(i, 'lnS') + if (info == nil) then break end + --print(json.encode(info)) + + local src = info.source + if string.sub(src, 1, 1) == '@' then + src = string.sub(src, 2) -- 앞의 '@' 떼어내기 + end + + local name + if info.name then + name = info.name .. ' (' .. (info.namewhat or '?') .. ')' + else + name = '?' + end + + local sframe = { + name = name, + source = { + name = nil, + path = Path.toAbsolute(sourceBasePath, src) + }, + column = 1, + line = info.currentline or 1, + id = i, + } + stackFrames[#stackFrames + 1] = sframe + end + + sendSuccess(req, { + stackFrames = stackFrames + }) +end + +------------------------------------------------------------------------------- +local scopeTypes = { + Locals = 1, + Upvalues = 2, + Globals = 3, +} +function handlers.scopes(req) + local depth = req.arguments.frameId + + local scopes = {} + local function addScope(name) + scopes[#scopes + 1] = { + name = name, + expensive = false, + variablesReference = depth * 1000000 + scopeTypes[name] + } + end + + addScope('Locals') + addScope('Upvalues') + addScope('Globals') + + sendSuccess(req, { + scopes = scopes + }) +end + +------------------------------------------------------------------------------- +local function registerVar(varNameCount, name_, value, noQuote) + local ty = type(value) + local name + if type(name_) == 'number' then + name = '[' .. name_ .. ']' + else + name = tostring(name_) + end + if varNameCount[name] then + varNameCount[name] = varNameCount[name] + 1 + name = name .. ' (' .. varNameCount[name] .. ')' + else + varNameCount[name] = 1 + end + + local item = { + name = name, + type = ty + } + + if (ty == 'string' and (not noQuote)) then + item.value = '"' .. value .. '"' + else + item.value = tostring(value) + end + + if (ty == 'table') or + (ty == 'function') or + (ty == 'userdata') then + storedVariables[nextVarRef] = value + item.variablesReference = nextVarRef + nextVarRef = nextVarRef + 1 + else + item.variablesReference = -1 + end + + return item +end + +------------------------------------------------------------------------------- +function handlers.variables(req) + local varRef = req.arguments.variablesReference + local variables = {} + local varNameCount = {} + local function addVar(name, value, noQuote) + variables[#variables + 1] = registerVar(varNameCount, name, value, noQuote) + end + + if (varRef >= 1000000) then + -- Scope. + local depth = math.floor(varRef / 1000000) + local scopeType = varRef % 1000000 + if scopeType == scopeTypes.Locals then + for i = 1, 9999 do + local name, value = debug_getlocal(depth, i) + if name == nil then break end + addVar(name, value, nil) + end + elseif scopeType == scopeTypes.Upvalues then + local info = debug_getinfo(depth, 'f') + if info and info.func then + for i = 1, 9999 do + local name, value = debug.getupvalue(info.func, i) + if name == nil then break end + addVar(name, value, nil) + end + end + elseif scopeType == scopeTypes.Globals then + for name, value in pairs(_G) do + addVar(name, value) + end + table.sort(variables, function(a, b) return a.name < b.name end) + end + else + -- Expansion. + local var = storedVariables[varRef] + if type(var) == 'table' then + for k, v in pairs(var) do + addVar(k, v) + end + table.sort(variables, function(a, b) + local aNum, aMatched = string.gsub(a.name, '^%[(%d+)%]$', '%1') + local bNum, bMatched = string.gsub(b.name, '^%[(%d+)%]$', '%1') + + if (aMatched == 1) and (bMatched == 1) then + -- both are numbers. compare numerically. + return tonumber(aNum) < tonumber(bNum) + elseif aMatched == bMatched then + -- both are strings. compare alphabetically. + return a.name < b.name + else + -- string comes first. + return aMatched < bMatched + end + end) + elseif type(var) == 'function' then + local info = debug.getinfo(var, 'S') + addVar('(source)', tostring(info.short_src), true) + addVar('(line)', info.linedefined) + + for i = 1, 9999 do + local name, value = debug.getupvalue(var, i) + if name == nil then break end + addVar(name, value) + end + elseif type(var) == 'userdata' then + addUserdataVar(var, addVar) + end + + local mt = getmetatable(var) + if mt then + addVar("(metatable)", mt) + end + end + + sendSuccess(req, { + variables = variables + }) +end + +------------------------------------------------------------------------------- +function handlers.continue(req) + sendSuccess(req, {}) + return 'CONTINUE' +end + +------------------------------------------------------------------------------- +local function stackHeight() + for i = 1, 9999999 do + if (debug_getinfo(i, '') == nil) then + return i + end + end +end + +------------------------------------------------------------------------------- +local stepTargetHeight = nil +local function step() + if (stepTargetHeight == nil) or (stackHeight() <= stepTargetHeight) then + breaker.setLineBreak(nil) + baseDepth = breaker.stackOffset.stepDebugLoop + startDebugLoop() + end +end + +------------------------------------------------------------------------------- +function handlers.next(req) + stepTargetHeight = stackHeight() - breaker.stackOffset.step + breaker.setLineBreak(step) + sendSuccess(req, {}) + return 'CONTINUE' +end + +------------------------------------------------------------------------------- +function handlers.stepIn(req) + stepTargetHeight = nil + breaker.setLineBreak(step) + sendSuccess(req, {}) + return 'CONTINUE' +end + +------------------------------------------------------------------------------- +function handlers.stepOut(req) + stepTargetHeight = stackHeight() - (breaker.stackOffset.step + 1) + breaker.setLineBreak(step) + sendSuccess(req, {}) + return 'CONTINUE' +end + +------------------------------------------------------------------------------- +function handlers.evaluate(req) + -- 실행할 소스 코드 준비 + local sourceCode = req.arguments.expression + if string.sub(sourceCode, 1, 1) == '!' then + sourceCode = string.sub(sourceCode, 2) + else + sourceCode = 'return (' .. sourceCode .. ')' + end + + -- 환경 준비. + -- 뭘 요구할지 모르니까 로컬, 업밸류, 글로벌을 죄다 복사해둔다. + -- 우선순위는 글로벌-업밸류-로컬 순서니까 + -- 그 반대로 갖다놓아서 나중 것이 앞의 것을 덮어쓰게 한다. + local depth = req.arguments.frameId + local tempG = {} + local declared = {} + local function set(k, v) + tempG[k] = v + declared[k] = true + end + + for name, value in pairs(_G) do + set(name, value) + end + + if depth then + local info = debug_getinfo(depth, 'f') + if info and info.func then + for i = 1, 9999 do + local name, value = debug.getupvalue(info.func, i) + if name == nil then break end + set(name, value) + end + end + + for i = 1, 9999 do + local name, value = debug_getlocal(depth, i) + if name == nil then break end + set(name, value) + end + else + -- VSCode가 depth를 안 보낼 수도 있다. + -- 특정 스택 프레임을 선택하지 않은, 전역 이름만 조회하는 경우이다. + end + local mt = { + __newindex = function() error('assignment not allowed', 2) end, + __index = function(t, k) if not declared[k] then error('not declared', 2) end end + } + setmetatable(tempG, mt) + + -- 파싱 + -- loadstring for Lua 5.1 + -- load for Lua 5.2 and 5.3(supports the private environment's load function) + local fn, err = (loadstring or load)(sourceCode, 'X', nil, tempG) + if fn == nil then + sendFailure(req, string.gsub(err, '^%[string %"X%"%]%:%d+%: ', '')) + return + end + + -- 실행하고 결과 송신 + if setfenv ~= nil then + -- Only for Lua 5.1 + setfenv(fn, tempG) + end + + local success, aux = pcall(fn) + if not success then + aux = aux or '' -- Execution of 'error()' returns nil as aux + sendFailure(req, string.gsub(aux, '^%[string %"X%"%]%:%d+%: ', '')) + return + end + + local varNameCount = {} + local item = registerVar(varNameCount, '', aux) + + sendSuccess(req, { + result = item.value, + type = item.type, + variablesReference = item.variablesReference + }) +end + +------------------------------------------------------------------------------- +return debuggee