Skip to content

fix(app): make the Streamlit front end run again - #71

Open
izzet wants to merge 4 commits into
llnl:developfrom
izzet:feat/streamlit-app
Open

fix(app): make the Streamlit front end run again#71
izzet wants to merge 4 commits into
llnl:developfrom
izzet:feat/streamlit-app

Conversation

@izzet

@izzet izzet commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Why

streamlit_app.py has not run since f102ef8 (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:

Breakage Detail
dask_jobqueue signal handler dask_jobqueue/runner.py calls signal.signal(SIGINT, ...) at import. Streamlit runs scripts off the main thread, where that raises ValueError: signal only works in main thread. import dftracer.analyzer fails in any embedder that isn't the main thread.
from dftracer.analyzer.rules import KnownCharacteristics rules.py no longer exists
constants.XFER_SIZE_BIN_LABELS renamed to SIZE_BIN_LABELS
result.characteristics AnalysisResult has no such field
threshold= hydra override no longer a config key
assets/logo.png never tracked in git
no preset override AnalyzerConfig.preset is MISSING, so the run could not have started
swapped labels file_count shown as "# of Processes", proc_count as "# of Files"
bottlenecks never assigned; the tab rendered None
undeclared deps streamlit and altair appear nowhere in packaging

What

cluster.py — make the dask_jobqueue import typing-only. The three classes were used solely in the ClusterType union; 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 every dftracer.analyzer load.

streamlit_app.py — rewritten against the current API. It now renders the JSON output payload (output=json), the same structure tests/test_e2e.py::test_json_output_file already asserts, so the UI tracks a tested schema instead of reaching into internals that move underneath it.

pyproject.toml — new web extra: pip install ".[web]".

tests/test_streamlit_app.py — a smoke render test (3.2s, catches every import breakage above) and a full test that uploads a real trace and asserts the counts.

Removed, deliberately

  • Bottlenecks tab — built on result.characteristics, which no longer exists. The nearest current equivalent is result.analysis_facts, but that is gated behind facts.enabled and has its own schema; wiring it up is a follow-up, not a silent substitution.
  • Transfer-size distribution charts — depended on XFER_SIZE_BIN_LABELS. I checked a live run: flat views for these presets contain no size_bin columns, so there is no data to plot. Better absent than fabricated.
  • Threshold sliderthreshold is not a config key anymore.
  • Logoassets/logo.png was never in the repo.

Verification

Driven headlessly with AppTest, and served for real with streamlit run --server.headless:

Check Result
First render 0 exceptions; title, uploader, preset selector, Analyze button present
Real server /_stcore/healthok, page → HTTP 200, no errors in the server log
Single file (dftracer-posix) 2,056 events / 1 process — matches tests/test_e2e.py
8 files (dftracer-dlio-prev, dlio-prev preset) 18,039 events / 8 processes, runtime 56.70s — matches the e2e ground truth and the README's Job Time 56.695s
Layer breakdown renders for both perspectives
Existing suite pytest -m smoke green on this branch
CLI dfanalyzer --help and both entrypoints still resolve; cluster=slurm|lsf|pbs targets still resolve via Hydra

Resource 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_workers was already pinned to 1 (for #69); this adds processes=False so 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:

fixture before after reduction
dftracer-posix (2,056 events) 489 MB 280 MB 1.7x
dftracer-dlio-prev (18,039 events) 601 MB 443 MB 1.4x

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, because maxUploadSize is 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 smoke green (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 develop today. If this lands first, #68 will conflict on two files. Both resolutions are mechanical:

If #68 lands first instead, this branch rebases onto it the same way.

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 30.26%. Comparing base (b4d0299) to head (23fabad).
⚠️ Report is 7 commits behind head on develop.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@izzet izzet self-assigned this Jul 26, 2026
@izzet izzet added the enhancement New feature or request label Jul 26, 2026
izzet added 4 commits July 27, 2026 12:28
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
izzet force-pushed the feat/streamlit-app branch from 6df9462 to 21207bf Compare July 27, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant