fix(app): make the Streamlit front end run again - #71
Open
izzet wants to merge 4 commits into
Open
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #71 +/- ##
===========================================
+ Coverage 29.91% 30.26% +0.35%
===========================================
Files 30 27 -3
Lines 3771 3522 -249
===========================================
- Hits 1128 1066 -62
+ Misses 2643 2456 -187 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
streamlit_app.py has not run since f102ef8 (2025-10-21, 162 commits ago). Nothing in the suite imported it, so it rotted silently. The deepest breakage is not in the app. dask_jobqueue installs a SIGINT handler at import time, and signal.signal() raises off the main thread -- which is where Streamlit runs user scripts. So `import dftracer.analyzer` failed in any embedder that is not the main thread. cluster.py only needed those three classes for the ClusterType union, and Hydra instantiates them from their _target_ strings, so the import is now typing-only. That also drops a heavy import from every analyzer load. On top of that the app referenced four things that no longer exist: dftracer.analyzer.rules, constants.XFER_SIZE_BIN_LABELS, AnalysisResult.characteristics, and a `threshold` hydra key. It also never passed a preset, which is MISSING by default, so the run could not have started; it read assets/logo.png, which was never tracked; and it labelled file_count as "# of Processes" and proc_count as "# of Files". Rewritten to render the JSON output payload -- the structure test_json_output_file already asserts -- so the UI tracks a tested schema instead of internals that move underneath it. Adds a preset selector, persists results in session_state, surfaces failures instead of blanking, and pins one Dask worker by default because multi-worker reads can silently truncate (llnl#69). The bottlenecks tab and the transfer-size charts are dropped rather than faked: they were built on APIs that are gone, and a live run confirms the flat views carry no size_bin columns for these presets. Adds an `app` extra, a 3-second render test that catches every import breakage above, and a slower test that uploads a real trace and asserts 2,056 events. Verified with streamlit run and with AppTest: dftracer-posix gives 2,056 events / 1 process and the 8-file dftracer-dlio-prev gives 18,039 events / 8 processes at 56.70s, matching the e2e ground truth and the README sample.
Without streamlit installed, tests/test_streamlit_app.py importorskips and CI reports it as skipped -- which reproduces exactly the blind spot that let the front end stay broken for 162 commits. Installing .[app] makes the 3-second render test actually execute.
"app" is ambiguous in a package that already is an application -- the dfanalyzer CLI is equally "the app". "web" says which interface the extra installs, and matches the README section heading.
Streamlit Community Cloud guarantees 690 MB of memory (2.7 GB ceiling) and 0.078-2 CPU cores. Measured against this repo's fixtures, the app did not fit, and nothing stopped a user handing it a trace that could not possibly complete. Three limits, each measured rather than guessed. One in-process worker. n_workers was already pinned to 1 (for llnl#69); this also sets processes=False, so the single worker stops being a separate process that re-imports pyarrow/pandas/dftracer. Peak RSS, identical settings otherwise: dftracer-posix (2,056 events) 489 MB -> 280 MB 1.7x dftracer-dlio-prev (18,039 events) 601 MB -> 443 MB 1.4x Both now clear the guaranteed allocation once the ~54 MB Streamlit server is added. With 2 cores at most, a process-based worker bought no parallelism. An explicit worker memory_limit. dask otherwise derives one from the host's RAM and core count, which in a container is the host's numbers rather than the container's -- on a 94 GiB / 40-core machine it autodetects 2.35 GiB against a 690 MB allocation, so the worker had no ceiling at all. Verified this does not backfire: no pause, spill or restart warnings on either fixture, and the golden values are unchanged (2,056 events / 1 process; 18,039 / 8). An upload cap, in a new .streamlit/config.toml (there was none, so Streamlit's 200 MB default applied) and again in the app across a whole submission, because maxUploadSize is enforced per file while the uploader accepts several. Over the cap is now an error naming the number and pointing at the CLI, instead of the container being OOM-killed with no explanation. A test asserts the two numbers stay in agreement. The cap is 2 MB, not the 16 MB used by the sibling wisio app. Peak RSS here is ~290 MB plus 11-17 MB per 1,000 events, so ~450 KB fits the guaranteed allocation and ~2.7 MB needs the best case; a 3.8 MB trace measured 3,499 MB. The hard cap sits at the best case and a warning fires at 450 KB: refuse what cannot complete, warn about what merely might not. Verified all three paths: 3.8 MB blocked with 0 metrics rendered, 981 KB warned and analyzed, 326 KB clean. Known weakness, unfixed: a byte cap is a poor proxy for a gzipped trace, since compressibility decides how many events those bytes become. Bounding event count would be accurate but requires reading the trace first, which is the expensive part. CPU is also likely to bite before memory does -- these timings come from a 40-core host, and throttled to a fraction of a core the same run could look hung.
izzet
force-pushed
the
feat/streamlit-app
branch
from
July 27, 2026 17:35
6df9462 to
21207bf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
streamlit_app.pyhas not run sincef102ef8(2025-10-21, 162 commits ago). It fails before rendering a single widget, and nothing in the suite imported it, so the rot was invisible.Every one of these is a hard failure on
develop:dask_jobqueuesignal handlerdask_jobqueue/runner.pycallssignal.signal(SIGINT, ...)at import. Streamlit runs scripts off the main thread, where that raisesValueError: signal only works in main thread.import dftracer.analyzerfails in any embedder that isn't the main thread.from dftracer.analyzer.rules import KnownCharacteristicsrules.pyno longer existsconstants.XFER_SIZE_BIN_LABELSSIZE_BIN_LABELSresult.characteristicsAnalysisResulthas no such fieldthreshold=hydra overrideassets/logo.pngAnalyzerConfig.presetisMISSING, so the run could not have startedfile_countshown as "# of Processes",proc_countas "# of Files"bottlenecksNonestreamlitandaltairappear nowhere in packagingWhat
cluster.py— make thedask_jobqueueimport typing-only. The three classes were used solely in theClusterTypeunion; Hydra instantiates them from their_target_strings, so they were never needed at import time. This is the fix that makes the package importable off the main thread; it also drops a heavy import from everydftracer.analyzerload.streamlit_app.py— rewritten against the current API. It now renders the JSON output payload (output=json), the same structuretests/test_e2e.py::test_json_output_filealready asserts, so the UI tracks a tested schema instead of reaching into internals that move underneath it..pfw.gzonly — uncompressed.pfwparses to nothing (fix(tests): gzip the trace data inside the stale dftracer fixtures #63), so offering it invites a silent empty resultposix,dlio,dlio-prev,generic) — previously absent, and requiredst.session_state, so they survive a widget interactionst.errorinstead of a blank pagepyproject.toml— newwebextra:pip install ".[web]".tests/test_streamlit_app.py— asmokerender test (3.2s, catches every import breakage above) and afulltest that uploads a real trace and asserts the counts.Removed, deliberately
result.characteristics, which no longer exists. The nearest current equivalent isresult.analysis_facts, but that is gated behindfacts.enabledand has its own schema; wiring it up is a follow-up, not a silent substitution.XFER_SIZE_BIN_LABELS. I checked a live run: flat views for these presets contain nosize_bincolumns, so there is no data to plot. Better absent than fabricated.thresholdis not a config key anymore.assets/logo.pngwas never in the repo.Verification
Driven headlessly with
AppTest, and served for real withstreamlit run --server.headless:/_stcore/health→ok, page → HTTP 200, no errors in the server logdftracer-posix)tests/test_e2e.pydftracer-dlio-prev,dlio-prevpreset)Job Time 56.695spytest -m smokegreen on this branchdfanalyzer --helpand both entrypoints still resolve;cluster=slurm|lsf|pbstargets still resolve via HydraResource limits for hosted deployment
Streamlit Community Cloud guarantees 690 MB of memory (2.7 GB ceiling) and 0.078-2 CPU cores. The app did not fit, and nothing stopped a user submitting a trace that could not complete. Three limits, measured rather than guessed:
One in-process worker.
n_workerswas already pinned to 1 (for #69); this addsprocesses=Falseso the worker stops being a separate process re-importing pyarrow/pandas/dftracer. With 2 cores at most it bought no parallelism. Peak RSS, identical settings otherwise:dftracer-posix(2,056 events)dftracer-dlio-prev(18,039 events)Both clear the guaranteed allocation once the ~54 MB Streamlit server is added.
An explicit worker
memory_limit. dask otherwise derives one from the host's RAM and core count, which in a container is the host's numbers — on a 94 GiB / 40-core machine it autodetects 2.35 GiB against a 690 MB allocation, so the worker had no ceiling at all. Verified it does not backfire: no pause, spill or restart warnings on either fixture, golden values unchanged (2,056 events / 1 process; 18,039 / 8).An upload cap in a new
.streamlit/config.toml(there was none, so Streamlit's 200 MB default applied) and again across a whole submission in the app, becausemaxUploadSizeis enforced per file while the uploader accepts several. Over the cap is an error naming the number and pointing at the CLI, not an OOM with no explanation. A test asserts the two numbers stay in agreement.The cap is 2 MB. Peak RSS is ~290 MB plus 11-17 MB per 1,000 events, so ~450 KB fits the guaranteed allocation and ~2.7 MB needs the best case; a 3.8 MB trace measured 3,499 MB. The hard cap sits at the best case with a warning at 450 KB: refuse what cannot complete, warn about what merely might not. (The sibling wisio app uses 16 MB; its read path is far lighter, so the number does not carry across.)
All three paths verified: 3.8 MB blocked with 0 metrics rendered, 981 KB warned and analyzed, 326 KB clean.
pytest -m smokegreen (157 passed).Known weaknesses, documented rather than silently accepted: a byte cap is a poor proxy for a gzipped trace, since compressibility decides how many events those bytes become — bounding event count would be accurate but requires reading the trace first. And CPU will likely bite before memory does; these timings are from a 40-core host.
Interaction with #68
Merges clean into
developtoday. If this lands first, #68 will conflict on two files. Both resolutions are mechanical:streamlit_app.py— keep the rewrite. It already satisfies the review point on chore!: drop Meson and build as a pure Python package #68 (.pfw.gzonly), so chore!: drop Meson and build as a pure Python package #68's one-line uploader change is redundant..github/workflows/ci.yml— combine: chore!: drop Meson and build as a pure Python package #68 drops the meson-Csetup-args, this PR adds the[web]extra, so the merged line ispip install ".[web]"with no setup-args.If #68 lands first instead, this branch rebases onto it the same way.