diff --git a/07_web_endpoints/fasthtml-checkboxes/checkboxes-load-test.py b/07_web_endpoints/fasthtml-checkboxes/checkboxes-load-test.py new file mode 100644 index 000000000..ea295a4a7 --- /dev/null +++ b/07_web_endpoints/fasthtml-checkboxes/checkboxes-load-test.py @@ -0,0 +1,95 @@ +import os +from datetime import datetime +from pathlib import Path + +import modal + +if modal.is_local(): + workspace = modal.config._profile +else: + workspace = os.environ["MODAL_WORKSPACE"] + + +image = ( + modal.Image.debian_slim(python_version="3.12") + .pip_install("locust~=2.29.1") + .env({"MODAL_WORKSPACE": workspace}) + .copy_local_file( + Path(__file__).parent / "checkboxes-locustfile.py", + remote_path="/root/locustfile.py", + ) + .copy_local_file( + Path(__file__).parent / "constants.py", + remote_path="/root/constants.py", + ) +) +volume = modal.Volume.from_name( + "loadtest-checkboxes-results", create_if_missing=True +) +remote_path = Path("/root") / "loadtests" +OUT_DIRECTORY = ( + remote_path / datetime.utcnow().replace(microsecond=0).isoformat() +) + +app = modal.App("loadtest-checkbox", image=image, volumes={remote_path: volume}) + +workers = 8 +host = f"https://{workspace}--example-checkboxes-web.modal.run" +csv_file = OUT_DIRECTORY / "stats.csv" +default_args = [ + "-H", + host, + "--processes", + str(workers), + "--csv", + csv_file, +] + +MINUTES = 60 # seconds + + +@app.function(allow_concurrent_inputs=1000, cpu=workers) +@modal.web_server(port=8089) +def serve(): + run_locust.local(default_args) + + +@app.function(cpu=workers, timeout=60 * MINUTES) +def run_locust(args: list, wait=False): + import subprocess + + process = subprocess.Popen(["locust"] + args) + if wait: + process.wait() + return process.returncode + + +@app.local_entrypoint() +def main( + r: float = 1.0, + u: int = 36, + t: str = "1m", # no more than the timeout of run_locust, one hour +): + args = default_args + [ + "--spawn-rate", + str(r), + "--users", + str(u), + "--run-time", + t, + ] + + html_report_file = OUT_DIRECTORY / "report.html" + args += [ + "--headless", # run without browser UI + "--autostart", # start test immediately + "--autoquit", # stop once finished... + "10", # ...but wait ten seconds + "--html", # output an HTML-formatted report + html_report_file, # to this location + ] + + if exit_code := run_locust.remote(args, wait=True): + SystemExit(exit_code) + else: + print("finished successfully") diff --git a/07_web_endpoints/fasthtml-checkboxes/checkboxes-locustfile.py b/07_web_endpoints/fasthtml-checkboxes/checkboxes-locustfile.py new file mode 100644 index 000000000..5486600e8 --- /dev/null +++ b/07_web_endpoints/fasthtml-checkboxes/checkboxes-locustfile.py @@ -0,0 +1,50 @@ +# --- +# lambda-test: false +# pytest: false +# --- +import random + +from constants import N_CHECKBOXES +from locust import HttpUser, between, task + + +class CheckboxesUser(HttpUser): + wait_time = between(0.01, 0.1) # Simulates a wait time between requests + + def load_homepage(self): + """ + Simulates a user loading the homepage and fetching the state of the checkboxes. + """ + self.client.get("/") + + @task(10) + def toggle_random_checkboxes(self): + """ + Simulates a user toggling a random checkbox. + """ + n_checkboxes = random.binomialvariate( # approximately poisson at 5 + n=100, + p=0.1, + ) + for _ in range(min(n_checkboxes, 1)): + checkbox_id = int( + N_CHECKBOXES * random.random() ** 2 + ) # Choose a random checkbox between 0 and 9999, more likely to be closer to 0 + self.client.post( + f"/checkbox/toggle/{checkbox_id}/{self.id}", + name="/checkbox/toggle", + ) + + @task(1) + def poll_for_diffs(self): + """ + Simulates a user polling for any outstanding diffs. + """ + self.client.get(f"/diffs/{self.id}", name="/diffs") + + def on_start(self): + """ + Called when a simulated user starts, typically used to initialize or login a user. + """ + self.id = str(random.randint(1, 9999)) + self.load_homepage() diff --git a/07_web_endpoints/fasthtml-checkboxes/constants.py b/07_web_endpoints/fasthtml-checkboxes/constants.py new file mode 100644 index 000000000..5597c2867 --- /dev/null +++ b/07_web_endpoints/fasthtml-checkboxes/constants.py @@ -0,0 +1,4 @@ +# --- +# lambda-test: false +# --- +N_CHECKBOXES = 10_000 # feel free to increase, if you dare! diff --git a/07_web_endpoints/fasthtml-checkboxes-ui.png b/07_web_endpoints/fasthtml-checkboxes/fasthtml-checkboxes-ui.png similarity index 100% rename from 07_web_endpoints/fasthtml-checkboxes-ui.png rename to 07_web_endpoints/fasthtml-checkboxes/fasthtml-checkboxes-ui.png diff --git a/07_web_endpoints/fasthtml_checkboxes.py b/07_web_endpoints/fasthtml-checkboxes/fasthtml_checkboxes.py similarity index 95% rename from 07_web_endpoints/fasthtml_checkboxes.py rename to 07_web_endpoints/fasthtml-checkboxes/fasthtml_checkboxes.py index 141f5cb16..e5018ee6e 100644 --- a/07_web_endpoints/fasthtml_checkboxes.py +++ b/07_web_endpoints/fasthtml-checkboxes/fasthtml_checkboxes.py @@ -1,6 +1,6 @@ # --- # deploy: true -# cmd: ["modal", "serve", "07_web_endpoints/fasthtml_checkboxes.py"] +# cmd: ["modal", "serve", "07_web_endpoints.fasthtml-checkboxes.fasthtml_checkboxes"] # mypy: ignore-errors # --- @@ -24,13 +24,13 @@ import modal +from .constants import N_CHECKBOXES + app = modal.App("example-checkboxes") db = modal.Dict.from_name("example-checkboxes-db", create_if_missing=True) -css_path_local = Path(__file__).parent / "fasthtml_checkboxes.css" -css_path_remote = Path("/assets/fasthtml_checkboxes.css") - -N_CHECKBOXES = 10_000 # feel free to increase, if you dare! +css_path_local = Path(__file__).parent / "styles.css" +css_path_remote = Path("/assets/styles.css") @app.function( diff --git a/07_web_endpoints/fasthtml_checkboxes.css b/07_web_endpoints/fasthtml-checkboxes/styles.css similarity index 100% rename from 07_web_endpoints/fasthtml_checkboxes.css rename to 07_web_endpoints/fasthtml-checkboxes/styles.css