{"id":17171020,"url":"https://github.com/tipsy/loomylin","last_synced_at":"2025-04-13T16:08:44.325Z","repository":{"id":61377152,"uuid":"315010779","full_name":"tipsy/loomylin","owner":"tipsy","description":null,"archived":false,"fork":false,"pushed_at":"2022-10-13T21:54:04.000Z","size":116,"stargazers_count":44,"open_issues_count":3,"forks_count":4,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-04-13T16:08:37.309Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tipsy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-11-22T10:27:17.000Z","updated_at":"2025-02-16T11:54:51.000Z","dependencies_parsed_at":"2022-10-16T08:39:31.907Z","dependency_job_id":null,"html_url":"https://github.com/tipsy/loomylin","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Floomylin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Floomylin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Floomylin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tipsy%2Floomylin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tipsy","download_url":"https://codeload.github.com/tipsy/loomylin/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248741205,"owners_count":21154255,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T23:33:22.412Z","updated_at":"2025-04-13T16:08:44.304Z","avatar_url":"https://github.com/tipsy.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# loomylin\n\n![Banner](gfx/banner.png?raw=true)\n\nLoomylin is a tech demo for [Project Loom](https://openjdk.java.net/projects/loom/),\nwhich brings lightweight user-mode threads (fibers/green-threads) to the JVM.\n\nIn the JDK, they refer to these threads as\n[Virtual Threads](https://wiki.openjdk.java.net/display/loom/Differences+between+regular+Threads+and+virtual+Threads).\nNot all operations are \"Virtual Thread Friendly\", you can find a list in the\n[OpenJDK Wiki](https://wiki.openjdk.java.net/pages/viewpage.action?pageId=46956620).\nThere's also a nice [Getting started](https://wiki.openjdk.java.net/display/loom/Getting+started) guide in the Wiki,\nbut this is not required reading to understand this project.\n\nThe idea behind this project is to gain knowledge of the practical value of running Loom \"in the wild\",\nby trying to use it with a traditional blocking HttpServlet server (in our case [Jetty](https://www.eclipse.org/jetty/)).\n\nThe project implements a [ThreadPool](https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html)\nwith virtual threads, passes this `ThreadPool` to a [Jetty](https://www.eclipse.org/jetty/) `Server`,\nand passes this Jetty `Server` to a [Javalin](https://javalin.io/) instance (for easy project setup).\n\nThe project has three servers:\n\n* A server with a `QueuedThreadPool`, 4 threads (the number of physical cores on my machine), running on port 7000\n* A server with a `QueuedThreadPool`, 32 threads, running on port 7001\n* A server with a custom `LoomThreadPool`, running on port 7002\n\nThe project contains instructions both for running with [Docker](https://www.docker.com/)\nand without (using [Maven Wrapper](https://github.com/takari/maven-wrapper), so please, pick your poison.\n\nThe project contains two equivalent implementations, one Java and one Kotlin.\n\n## Running with Docker\n\nThe fastest way to run the project (provided you have Docker installed), is to:\n\n```\ngit clone https://github.com/tipsy/loomylin/\ncd loomylin\ndocker build -t loomylin:1.0 .\ndocker run -p 7000:7000 -p 7001:7001 -p 7002:7002 --detach --name loomylin loomylin:1.0\n```\n\nAfter this you'll have all three servers running, and can proceed to load testing.\n\n## Running without Docker\n\nFirst, you'll need to grab the latest version of OpenJDK.\nThe easiest way to do this is to install [SDKMAN](https://sdkman.io/). This will also make it\neasy to switch back to a stable JDK later, if you need to.\n\nTo list all the available JDKs, you do:\n\n```shell\nsdk list java\n```\n\nLocate an SDK with Loom (ex: `19-open`), and do:\n\n```\nsdk install java 19-open\n```\n\nThat's it. Now you can either run the project via Maven Wrapper by doing:\n\n```\n./mvnw clean install\njava --enable-preview -jar target/loomylin-jar-with-dependencies.jar\n```\n\nOr open the project in your favorite IDE. If you get `Error: Cannot determine path to\n'tools.jar' library for 16 (path/to/jdk-16)` when running from IDEA, you should update to the latest version.\n\nThe main function in `src/main/kotlin/loomylin/Loomylin.kt` will start all three servers.\n\n## Load testing with hey\n\n_Disclaimer: I have very little experience with load testing, so take these results with a pinch of salt. Feedback to improve this section would be much appreciated!_\n\nYou can load test using whatever you want, this section just outlines a quick and easy approach\nwith the library [hey](https://github.com/rakyll/hey). If you want something from NPM,\n[autocannon](https://github.com/mcollina/autocannon) seems like a pretty good alternative.\n\nThe `hey` tool runs requests per worker sequentially, so if each worker has 20 requests to run,\nand each request takes 100ms, that worker should be done in 2s.\n\nTo warm everything up, all tests are run thrice in a for-loop.\nOnly the third result is included below.\n\n### Computational endpoint\n\nThis endpoint runs `Math.random()` 25000 times, then returns \"Hello, World\".\nThis number was chosen because it made the tests bearable to run.\n\n#### QueuedThreadPool (4 threads)\n\n```\nfor i in {1..3}; do hey -n 1000 -c 50 -m GET http://localhost:7000/computational; done\n\nSummary\n  Total:    2.0975 secs\n  Slowest:  0.2383 secs\n  Fastest:  0.0011 secs\n  Average:  0.0800 secs\n  Requests/sec: 476.7507\n\n  Total data:   14000 bytes\n  Size/request: 14 bytes\n\nResponse time histogram:\n  0.001 [1]     |\n  0.025 [491]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.049 [14]    |■\n  0.072 [29]    |■■\n  0.096 [54]    |■■■■\n  0.120 [51]    |■■■■\n  0.143 [43]    |■■■■\n  0.167 [99]    |■■■■■■■■\n  0.191 [56]    |■■■■■\n  0.215 [112]   |■■■■■■■■■\n  0.238 [50]    |■■■■\n```\n\n#### QueuedThreadPool (32 threads)\n\n```\nfor i in {1..3}; do hey -n 1000 -c 50 -m GET http://localhost:7001/computational; done\n\nSummary:\n  Total:    5.8990 secs\n  Slowest:  0.9932 secs\n  Fastest:  0.0038 secs\n  Average:  0.2530 secs\n  Requests/sec: 169.5216\n\n  Total data:   14000 bytes\n  Size/request: 14 bytes\n\nResponse time histogram:\n  0.004 [1]     |\n  0.103 [137]   |■■■■■■■■■■■\n  0.202 [481]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.301 [167]   |■■■■■■■■■■■■■■\n  0.400 [47]    |■■■■\n  0.498 [40]    |■■■\n  0.597 [15]    |■\n  0.696 [19]    |■■\n  0.795 [45]    |■■■■\n  0.894 [35]    |■■■\n  0.993 [13]    |■\n```\n\n#### LoomThreadPool (infinite threads)\n\n```\nfor i in {1..3}; do hey -n 1000 -c 50 -m GET http://localhost:7002/computational; done\n\nSummary:\n  Total:    3.7353 secs\n  Slowest:  2.6117 secs\n  Fastest:  0.0119 secs\n  Average:  0.1413 secs\n  Requests/sec:\t267.7162\n\n  Total data:   14000 bytes\n  Size/request: 14 bytes\n\nResponse time histogram:\n  0.012 [1]     |\n  0.272 [983]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.532 [2]     |\n  0.792 [2]     |\n  1.052 [0]     |\n  1.312 [0]     |\n  1.572 [0]     |\n  1.832 [0]     |\n  2.092 [0]     |\n  2.352 [0]     |\n  2.612 [12]    |\n```\n\n#### Discussion\n\nWhen doing pure computation, having number of threads equal to the number of\nphysical cores should be the most efficient. Using 32 threads is almost three times as slow,\nmost likely because the overhead introduced by thread switching (and hyper-threading).\n\nI expected the Loom server to be similar in performance to 4 threads, but it's almost twice as slow.\nYou can see that the request time is super consistent at 272ms, but there are a few\noutliers in the results that ruin everything (12 requests took 2.6s to finish). These outliers\nmight be slow because of garbage collection, but I haven't had the time to look into that yet.\n\n### Waiting endpoint (blocking)\n\nThis endpoint is where Loom should shine. It does a simple `Thread.sleep(100)`, before\nsettings the result to \"Hello, World\".\n\n#### QueuedThreadPool (4 threads)\n\n```\nfor i in {1..3}; do hey -n 1000 -c 50 -m GET http://localhost:7000/wait-blocking; done\n\nSummary:\n  Total:    52.4542 secs\n  Slowest:  6.3712 secs\n  Fastest:  0.1005 secs\n  Average:  2.2467 secs\n  Requests/sec: 19.0642\n\n  Total data:   12000 bytes\n  Size/request: 12 bytes\n\nResponse time histogram:\n  0.100 [1]     |\n  0.728 [525]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  1.355 [15]    |■\n  1.982 [18]    |■\n  2.609 [20]    |■■\n  3.236 [19]    |■\n  3.863 [15]    |■\n  4.490 [55]    |■■■■\n  5.117 [175]   |■■■■■■■■■■■■■\n  5.744 [87]    |■■■■■■■\n  6.371 [70]    |■■■■■\n```\n\n#### QueuedThreadPool (32 threads)\n\n```\nfor i in {1..3}; do hey -n 1000 -c 50 -m GET http://localhost:7001/wait-blocking; done\n\nSummary:\n  Total:    3.8441 secs\n  Slowest:  0.3177 secs\n  Fastest:  0.1004 secs\n  Average:  0.1733 secs\n  Requests/sec:\t260.1360\n\n  Total data:   12000 bytes\n  Size/request: 12 bytes\n\nResponse time histogram:\n  0.100 [1]     |\n  0.122 [361]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.144 [0]     |\n  0.166 [0]     |\n  0.187 [0]     |\n  0.209 [381]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.231 [228]   |■■■■■■■■■■■■■■■■■■■■■■■■\n  0.253 [0]     |\n  0.274 [0]     |\n  0.296 [7]     |■\n  0.318 [22]    |■■\n```\n\n#### LoomThreadPool (infinite threads)\n\n```\nfor i in {1..3}; do hey -n 1000 -c 50 -m GET http://localhost:7002/wait-blocking; done\n\nSummary:\n  Total:    2.0853 secs\n  Slowest:  0.1082 secs\n  Fastest:  0.1005 secs\n  Average:  0.1042 secs\n  Requests/sec: 479.5443\n\n  Total data:   12000 bytes\n  Size/request: 12 bytes\n\nResponse time histogram:\n  0.100 [1]     |\n  0.101 [41]    |■■■■■■■■\n  0.102 [119]   |■■■■■■■■■■■■■■■■■■■■■■■\n  0.103 [128]   |■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.104 [87]    |■■■■■■■■■■■■■■■■■\n  0.104 [85]    |■■■■■■■■■■■■■■■■■\n  0.105 [143]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.106 [204]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.107 [121]   |■■■■■■■■■■■■■■■■■■■■■■■■\n  0.107 [48]    |■■■■■■■■■\n  0.108 [23]    |■■■■■\n```\n\n#### Discussion\nThe 4 thread server is basically useless in this scenario. In theory it should spend\n`1000 * 100` `/` `min(4, 50)` `=` `25s`, but it gets severely clogged and stuck switching between threads,\nspending almost a full minute on servicing the 1000 requests.\n\nThe 32 thread server should spend `1000 * 100` `/` `min(32, 50)` `=` `3.1s`, but\nsince there are more clients than threads, some thread switching and waiting does occur,\nand it ends up spending 3.8s.\n\nThe infinite thread server (Loom) should spend `1000 * 100` `/` `min(infinity, 50)` `=` `2s`,\nand in this case the number of clients is the limiting factor (50). For once reality\nis close to theory, and the server finishes in 2.1s.\n\nThese are the type of scenarios where Loom should be very useful.\nIf your server is spending a lot of time waiting for blocking calls, plugging in Loom to\nreplace a `QueuedThreadPool` would give you instants results, without any further need\nfor configuration.\n\n### BONUS: Waiting endpoint (async)\n\nFor fun, there's also an async endpoint attached to the servers. It doesn't matter which\nserver you test this against, as async requests are lifted out of the Jetty's request `ThreadPool`.\nWhen running our async test we should get results more or less identical to the Loom test:\n\n```\nfor i in {1..3}; do hey -n 1000 -c 50 -m GET http://localhost:7000/wait-async; done\n\nSummary:\n  Total:    2.0764 secs\n  Slowest:  0.1112 secs\n  Fastest:  0.1004 secs\n  Average:  0.1037 secs\n  Requests/sec:\t481.6112\n\n  Total data:   12000 bytes\n  Size/request: 12 bytes\n\nResponse time histogram:\n  0.100 [1]     |\n  0.101 [86]    |■■■■■■■■■■■\n  0.103 [169]   |■■■■■■■■■■■■■■■■■■■■■■\n  0.104 [313]   |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.105 [162]   |■■■■■■■■■■■■■■■■■■■■■\n  0.106 [161]   |■■■■■■■■■■■■■■■■■■■■■\n  0.107 [58]    |■■■■■■■\n  0.108 [0]     |\n  0.109 [13]    |■■\n  0.110 [23]    |■■■\n  0.111 [14]    |■■\n```\n\nAnd we do! The downside here is of course that you have to write your code using async concepts,\nso instead of a simple `Thread.sleep()` you have to create an executor, wrap your result in a\n`CompletableFuture` and schedule the completion of this future on your executor.\n\n### Conclusion\n\nSetting up Project Loom is surprisingly easy, but load testing is surprisingly hard.\nLoom seems like it could give huge performance gains for virtually no effort, but it's important\nto know when it's appropriate to use it. Remember that not all APIs are \"Loom friendly\",\nso consult the [OpenJDK Wiki](https://wiki.openjdk.java.net/pages/viewpage.action?pageId=46956620)\nbefore getting your hopes up.\n\nIf you have any suggestions for improvements or want to expand the test cases and/or load testing sections,\nplease submit an issue or PR, I would greatly appreciate it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftipsy%2Floomylin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftipsy%2Floomylin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftipsy%2Floomylin/lists"}