{"html_url": "https://github.com/simonw/datasette/issues/1723#issuecomment-1110330554", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1723", "id": 1110330554, "node_id": "IC_kwDOBm6k_c5CLky6", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-26T23:06:20Z", "updated_at": "2022-04-26T23:06:20Z", "author_association": "OWNER", "body": "Deployed here: https://latest-with-plugins.datasette.io/github/commits?_facet=repo&_trace=1&_facet=committer", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1216508080, "label": "Research running SQL in table view in parallel using `asyncio.gather()`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1723#issuecomment-1110305790", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1723", "id": 1110305790, "node_id": "IC_kwDOBm6k_c5CLev-", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-26T22:19:04Z", "updated_at": "2022-04-26T22:19:04Z", "author_association": "OWNER", "body": "I realized that seeing the total time in queries wasn't enough to understand this, because if the queries were executed in serial or parallel it should still sum up to the same amount of SQL time (roughly).\r\n\r\nInstead I need to know how long the page took to render. But that's hard to display on the page since you can't measure it until rendering has finished!\r\n\r\nSo I built an ASGI plugin to handle that measurement: https://github.com/simonw/datasette-total-page-time\r\n\r\nAnd with that plugin installed, `http://127.0.0.1:8001/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel2&_facet=other_fuel1&_parallel=1` (the parallel version) takes 377ms:\r\n\r\n\"CleanShot\r\n\r\nWhile `http://127.0.0.1:8001/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel2&_facet=other_fuel1` (the serial version) takes 762ms:\r\n\r\n\"image\"\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1216508080, "label": "Research running SQL in table view in parallel using `asyncio.gather()`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1723#issuecomment-1110279869", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1723", "id": 1110279869, "node_id": "IC_kwDOBm6k_c5CLYa9", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-26T21:45:39Z", "updated_at": "2022-04-26T21:45:39Z", "author_association": "OWNER", "body": "Getting some nice traces out of this:\r\n\r\n\"CleanShot\r\n\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1216508080, "label": "Research running SQL in table view in parallel using `asyncio.gather()`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1723#issuecomment-1110278577", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1723", "id": 1110278577, "node_id": "IC_kwDOBm6k_c5CLYGx", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-26T21:44:04Z", "updated_at": "2022-04-26T21:44:04Z", "author_association": "OWNER", "body": "And some simple benchmarks with `ab` - using the `?_parallel=1` hack to try it with and without a parallel `asyncio.gather()`:\r\n\r\n```\r\n~ % ab -n 100 'http://127.0.0.1:8001/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2' \r\nThis is ApacheBench, Version 2.3 <$Revision: 1879490 $>\r\nCopyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/\r\nLicensed to The Apache Software Foundation, http://www.apache.org/\r\n\r\nBenchmarking 127.0.0.1 (be patient).....done\r\n\r\n\r\nServer Software: uvicorn\r\nServer Hostname: 127.0.0.1\r\nServer Port: 8001\r\n\r\nDocument Path: /global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2\r\nDocument Length: 314187 bytes\r\n\r\nConcurrency Level: 1\r\nTime taken for tests: 68.279 seconds\r\nComplete requests: 100\r\nFailed requests: 13\r\n (Connect: 0, Receive: 0, Length: 13, Exceptions: 0)\r\nTotal transferred: 31454937 bytes\r\nHTML transferred: 31418437 bytes\r\nRequests per second: 1.46 [#/sec] (mean)\r\nTime per request: 682.787 [ms] (mean)\r\nTime per request: 682.787 [ms] (mean, across all concurrent requests)\r\nTransfer rate: 449.89 [Kbytes/sec] received\r\n\r\nConnection Times (ms)\r\n min mean[+/-sd] median max\r\nConnect: 0 0 0.0 0 0\r\nProcessing: 621 683 68.0 658 993\r\nWaiting: 620 682 68.0 657 992\r\nTotal: 621 683 68.0 658 993\r\n\r\nPercentage of the requests served within a certain time (ms)\r\n 50% 658\r\n 66% 678\r\n 75% 687\r\n 80% 711\r\n 90% 763\r\n 95% 879\r\n 98% 926\r\n 99% 993\r\n 100% 993 (longest request)\r\n\r\n\r\n----\r\n\r\nIn parallel:\r\n\r\n~ % ab -n 100 'http://127.0.0.1:8001/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=1'\r\nThis is ApacheBench, Version 2.3 <$Revision: 1879490 $>\r\nCopyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/\r\nLicensed to The Apache Software Foundation, http://www.apache.org/\r\n\r\nBenchmarking 127.0.0.1 (be patient).....done\r\n\r\n\r\nServer Software: uvicorn\r\nServer Hostname: 127.0.0.1\r\nServer Port: 8001\r\n\r\nDocument Path: /global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=1\r\nDocument Length: 315703 bytes\r\n\r\nConcurrency Level: 1\r\nTime taken for tests: 34.763 seconds\r\nComplete requests: 100\r\nFailed requests: 11\r\n (Connect: 0, Receive: 0, Length: 11, Exceptions: 0)\r\nTotal transferred: 31607988 bytes\r\nHTML transferred: 31570288 bytes\r\nRequests per second: 2.88 [#/sec] (mean)\r\nTime per request: 347.632 [ms] (mean)\r\nTime per request: 347.632 [ms] (mean, across all concurrent requests)\r\nTransfer rate: 887.93 [Kbytes/sec] received\r\n\r\nConnection Times (ms)\r\n min mean[+/-sd] median max\r\nConnect: 0 0 0.0 0 0\r\nProcessing: 311 347 28.0 338 450\r\nWaiting: 311 347 28.0 338 450\r\nTotal: 312 348 28.0 338 451\r\n\r\nPercentage of the requests served within a certain time (ms)\r\n 50% 338\r\n 66% 348\r\n 75% 361\r\n 80% 367\r\n 90% 396\r\n 95% 408\r\n 98% 436\r\n 99% 451\r\n 100% 451 (longest request)\r\n\r\n----\r\n\r\nWith concurrency 10, not parallel:\r\n\r\n~ % ab -c 10 -n 100 'http://127.0.0.1:8001/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=' \r\nThis is ApacheBench, Version 2.3 <$Revision: 1879490 $>\r\nCopyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/\r\nLicensed to The Apache Software Foundation, http://www.apache.org/\r\n\r\nBenchmarking 127.0.0.1 (be patient).....done\r\n\r\n\r\nServer Software: uvicorn\r\nServer Hostname: 127.0.0.1\r\nServer Port: 8001\r\n\r\nDocument Path: /global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=\r\nDocument Length: 314346 bytes\r\n\r\nConcurrency Level: 10\r\nTime taken for tests: 38.408 seconds\r\nComplete requests: 100\r\nFailed requests: 93\r\n (Connect: 0, Receive: 0, Length: 93, Exceptions: 0)\r\nTotal transferred: 31471333 bytes\r\nHTML transferred: 31433733 bytes\r\nRequests per second: 2.60 [#/sec] (mean)\r\nTime per request: 3840.829 [ms] (mean)\r\nTime per request: 384.083 [ms] (mean, across all concurrent requests)\r\nTransfer rate: 800.18 [Kbytes/sec] received\r\n\r\nConnection Times (ms)\r\n min mean[+/-sd] median max\r\nConnect: 0 0 0.1 0 1\r\nProcessing: 685 3719 354.0 3774 4096\r\nWaiting: 684 3707 353.7 3750 4095\r\nTotal: 685 3719 354.0 3774 4096\r\n\r\nPercentage of the requests served within a certain time (ms)\r\n 50% 3774\r\n 66% 3832\r\n 75% 3855\r\n 80% 3878\r\n 90% 3944\r\n 95% 4006\r\n 98% 4057\r\n 99% 4096\r\n 100% 4096 (longest request)\r\n\r\n\r\n----\r\n\r\nConcurrency 10 parallel:\r\n\r\n~ % ab -c 10 -n 100 'http://127.0.0.1:8001/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=1'\r\nThis is ApacheBench, Version 2.3 <$Revision: 1879490 $>\r\nCopyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/\r\nLicensed to The Apache Software Foundation, http://www.apache.org/\r\n\r\nBenchmarking 127.0.0.1 (be patient).....done\r\n\r\n\r\nServer Software: uvicorn\r\nServer Hostname: 127.0.0.1\r\nServer Port: 8001\r\n\r\nDocument Path: /global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=1\r\nDocument Length: 315703 bytes\r\n\r\nConcurrency Level: 10\r\nTime taken for tests: 36.762 seconds\r\nComplete requests: 100\r\nFailed requests: 89\r\n (Connect: 0, Receive: 0, Length: 89, Exceptions: 0)\r\nTotal transferred: 31606516 bytes\r\nHTML transferred: 31568816 bytes\r\nRequests per second: 2.72 [#/sec] (mean)\r\nTime per request: 3676.182 [ms] (mean)\r\nTime per request: 367.618 [ms] (mean, across all concurrent requests)\r\nTransfer rate: 839.61 [Kbytes/sec] received\r\n\r\nConnection Times (ms)\r\n min mean[+/-sd] median max\r\nConnect: 0 0 0.1 0 0\r\nProcessing: 381 3602 419.6 3609 4458\r\nWaiting: 381 3586 418.7 3607 4457\r\nTotal: 381 3603 419.6 3609 4458\r\n\r\nPercentage of the requests served within a certain time (ms)\r\n 50% 3609\r\n 66% 3741\r\n 75% 3791\r\n 80% 3821\r\n 90% 3972\r\n 95% 4074\r\n 98% 4386\r\n 99% 4458\r\n 100% 4458 (longest request)\r\n\r\n\r\nTrying -c 3 instead. Non parallel:\r\n\r\n~ % ab -c 3 -n 100 'http://127.0.0.1:8001/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel='\r\nThis is ApacheBench, Version 2.3 <$Revision: 1879490 $>\r\nCopyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/\r\nLicensed to The Apache Software Foundation, http://www.apache.org/\r\n\r\nBenchmarking 127.0.0.1 (be patient).....done\r\n\r\n\r\nServer Software: uvicorn\r\nServer Hostname: 127.0.0.1\r\nServer Port: 8001\r\n\r\nDocument Path: /global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=\r\nDocument Length: 314346 bytes\r\n\r\nConcurrency Level: 3\r\nTime taken for tests: 39.365 seconds\r\nComplete requests: 100\r\nFailed requests: 83\r\n (Connect: 0, Receive: 0, Length: 83, Exceptions: 0)\r\nTotal transferred: 31470808 bytes\r\nHTML transferred: 31433208 bytes\r\nRequests per second: 2.54 [#/sec] (mean)\r\nTime per request: 1180.955 [ms] (mean)\r\nTime per request: 393.652 [ms] (mean, across all concurrent requests)\r\nTransfer rate: 780.72 [Kbytes/sec] received\r\n\r\nConnection Times (ms)\r\n min mean[+/-sd] median max\r\nConnect: 0 0 0.0 0 0\r\nProcessing: 731 1153 126.2 1189 1359\r\nWaiting: 730 1151 125.9 1188 1358\r\nTotal: 731 1153 126.2 1189 1359\r\n\r\nPercentage of the requests served within a certain time (ms)\r\n 50% 1189\r\n 66% 1221\r\n 75% 1234\r\n 80% 1247\r\n 90% 1296\r\n 95% 1309\r\n 98% 1343\r\n 99% 1359\r\n 100% 1359 (longest request)\r\n\r\n----\r\n\r\nParallel:\r\n\r\n~ % ab -c 3 -n 100 'http://127.0.0.1:8001/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=1'\r\nThis is ApacheBench, Version 2.3 <$Revision: 1879490 $>\r\nCopyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/\r\nLicensed to The Apache Software Foundation, http://www.apache.org/\r\n\r\nBenchmarking 127.0.0.1 (be patient).....done\r\n\r\n\r\nServer Software: uvicorn\r\nServer Hostname: 127.0.0.1\r\nServer Port: 8001\r\n\r\nDocument Path: /global-power-plants/global-power-plants?_facet=primary_fuel&_facet=other_fuel1&_facet=other_fuel3&_facet=other_fuel2&_parallel=1\r\nDocument Length: 315703 bytes\r\n\r\nConcurrency Level: 3\r\nTime taken for tests: 34.530 seconds\r\nComplete requests: 100\r\nFailed requests: 18\r\n (Connect: 0, Receive: 0, Length: 18, Exceptions: 0)\r\nTotal transferred: 31606179 bytes\r\nHTML transferred: 31568479 bytes\r\nRequests per second: 2.90 [#/sec] (mean)\r\nTime per request: 1035.902 [ms] (mean)\r\nTime per request: 345.301 [ms] (mean, across all concurrent requests)\r\nTransfer rate: 893.87 [Kbytes/sec] received\r\n\r\nConnection Times (ms)\r\n min mean[+/-sd] median max\r\nConnect: 0 0 0.0 0 0\r\nProcessing: 412 1020 104.4 1018 1280\r\nWaiting: 411 1018 104.1 1014 1275\r\nTotal: 412 1021 104.4 1018 1280\r\n\r\nPercentage of the requests served within a certain time (ms)\r\n 50% 1018\r\n 66% 1041\r\n 75% 1061\r\n 80% 1079\r\n 90% 1136\r\n 95% 1176\r\n 98% 1251\r\n 99% 1280\r\n 100% 1280 (longest request)\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1216508080, "label": "Research running SQL in table view in parallel using `asyncio.gather()`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1723#issuecomment-1110278182", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1723", "id": 1110278182, "node_id": "IC_kwDOBm6k_c5CLYAm", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-26T21:43:34Z", "updated_at": "2022-04-26T21:43:34Z", "author_association": "OWNER", "body": "Here's the diff I'm using:\r\n```diff\r\ndiff --git a/datasette/views/table.py b/datasette/views/table.py\r\nindex d66adb8..f15ef1e 100644\r\n--- a/datasette/views/table.py\r\n+++ b/datasette/views/table.py\r\n@@ -1,3 +1,4 @@\r\n+import asyncio\r\n import itertools\r\n import json\r\n \r\n@@ -5,6 +6,7 @@ import markupsafe\r\n \r\n from datasette.plugins import pm\r\n from datasette.database import QueryInterrupted\r\n+from datasette import tracer\r\n from datasette.utils import (\r\n await_me_maybe,\r\n CustomRow,\r\n@@ -150,6 +152,16 @@ class TableView(DataView):\r\n default_labels=False,\r\n _next=None,\r\n _size=None,\r\n+ ):\r\n+ with tracer.trace_child_tasks():\r\n+ return await self._data_traced(request, default_labels, _next, _size)\r\n+\r\n+ async def _data_traced(\r\n+ self,\r\n+ request,\r\n+ default_labels=False,\r\n+ _next=None,\r\n+ _size=None,\r\n ):\r\n database_route = tilde_decode(request.url_vars[\"database\"])\r\n table_name = tilde_decode(request.url_vars[\"table\"])\r\n@@ -159,6 +171,20 @@ class TableView(DataView):\r\n raise NotFound(\"Database not found: {}\".format(database_route))\r\n database_name = db.name\r\n \r\n+ # For performance profiling purposes, ?_parallel=1 turns on asyncio.gather\r\n+ async def _gather_parallel(*args):\r\n+ return await asyncio.gather(*args)\r\n+\r\n+ async def _gather_sequential(*args):\r\n+ results = []\r\n+ for fn in args:\r\n+ results.append(await fn)\r\n+ return results\r\n+\r\n+ gather = (\r\n+ _gather_parallel if request.args.get(\"_parallel\") else _gather_sequential\r\n+ )\r\n+\r\n # If this is a canned query, not a table, then dispatch to QueryView instead\r\n canned_query = await self.ds.get_canned_query(\r\n database_name, table_name, request.actor\r\n@@ -174,8 +200,12 @@ class TableView(DataView):\r\n write=bool(canned_query.get(\"write\")),\r\n )\r\n \r\n- is_view = bool(await db.get_view_definition(table_name))\r\n- table_exists = bool(await db.table_exists(table_name))\r\n+ is_view, table_exists = map(\r\n+ bool,\r\n+ await gather(\r\n+ db.get_view_definition(table_name), db.table_exists(table_name)\r\n+ ),\r\n+ )\r\n \r\n # If table or view not found, return 404\r\n if not is_view and not table_exists:\r\n@@ -497,33 +527,44 @@ class TableView(DataView):\r\n )\r\n )\r\n \r\n- if not nofacet:\r\n- for facet in facet_instances:\r\n- (\r\n+ async def execute_facets():\r\n+ if not nofacet:\r\n+ # Run them in parallel\r\n+ facet_awaitables = [facet.facet_results() for facet in facet_instances]\r\n+ facet_awaitable_results = await gather(*facet_awaitables)\r\n+ for (\r\n instance_facet_results,\r\n instance_facets_timed_out,\r\n- ) = await facet.facet_results()\r\n- for facet_info in instance_facet_results:\r\n- base_key = facet_info[\"name\"]\r\n- key = base_key\r\n- i = 1\r\n- while key in facet_results:\r\n- i += 1\r\n- key = f\"{base_key}_{i}\"\r\n- facet_results[key] = facet_info\r\n- facets_timed_out.extend(instance_facets_timed_out)\r\n-\r\n- # Calculate suggested facets\r\n+ ) in facet_awaitable_results:\r\n+ for facet_info in instance_facet_results:\r\n+ base_key = facet_info[\"name\"]\r\n+ key = base_key\r\n+ i = 1\r\n+ while key in facet_results:\r\n+ i += 1\r\n+ key = f\"{base_key}_{i}\"\r\n+ facet_results[key] = facet_info\r\n+ facets_timed_out.extend(instance_facets_timed_out)\r\n+\r\n suggested_facets = []\r\n- if (\r\n- self.ds.setting(\"suggest_facets\")\r\n- and self.ds.setting(\"allow_facet\")\r\n- and not _next\r\n- and not nofacet\r\n- and not nosuggest\r\n- ):\r\n- for facet in facet_instances:\r\n- suggested_facets.extend(await facet.suggest())\r\n+\r\n+ async def execute_suggested_facets():\r\n+ # Calculate suggested facets\r\n+ if (\r\n+ self.ds.setting(\"suggest_facets\")\r\n+ and self.ds.setting(\"allow_facet\")\r\n+ and not _next\r\n+ and not nofacet\r\n+ and not nosuggest\r\n+ ):\r\n+ # Run them in parallel\r\n+ facet_suggest_awaitables = [\r\n+ facet.suggest() for facet in facet_instances\r\n+ ]\r\n+ for suggest_result in await gather(*facet_suggest_awaitables):\r\n+ suggested_facets.extend(suggest_result)\r\n+\r\n+ await gather(execute_facets(), execute_suggested_facets())\r\n \r\n # Figure out columns and rows for the query\r\n columns = [r[0] for r in results.description]\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1216508080, "label": "Research running SQL in table view in parallel using `asyncio.gather()`"}, "performed_via_github_app": null}