expr_tracker 0.5.0__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/Cargo.lock +1 -1
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/Cargo.toml +1 -1
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/PKG-INFO +2 -2
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/README.md +1 -1
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/web-viewer.md +127 -22
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/pyproject.toml +2 -1
- expr_tracker-0.6.1/rust/viewer/aggregate.rs +217 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/auth_store.rs +27 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/index.rs +3 -1
- expr_tracker-0.6.1/rust/viewer/log_files.rs +30 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/stream.rs +13 -3
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/tests.rs +42 -1
- expr_tracker-0.6.1/rust/viewer/workspace.rs +157 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer.rs +78 -46
- expr_tracker-0.6.1/src/expr_tracker/_web/assets/curve-worker-B9xuMx07.js +2 -0
- expr_tracker-0.6.1/src/expr_tracker/_web/assets/index-BVS-StHr.js +57 -0
- expr_tracker-0.6.1/src/expr_tracker/_web/assets/index-BfrrriPo.css +1 -0
- expr_tracker-0.6.1/src/expr_tracker/_web/favicon.svg +3 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/index.html +3 -2
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_viewer.py +149 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/uv.lock +7 -7
- expr_tracker-0.6.1/web/e2e/viewer-fixtures.ts +194 -0
- expr_tracker-0.6.1/web/e2e/viewer-layout.spec.ts +573 -0
- expr_tracker-0.6.1/web/e2e/viewer-sources.spec.ts +468 -0
- expr_tracker-0.6.1/web/e2e/viewer-streaming.spec.ts +333 -0
- expr_tracker-0.6.1/web/e2e/viewer.spec.ts +571 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/index.html +1 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/package-lock.json +2 -2
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/package.json +1 -1
- expr_tracker-0.6.1/web/public/favicon.svg +3 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/api.test.ts +6 -2
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/api.ts +15 -1
- expr_tracker-0.6.1/web/src/app.tsx +269 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-export.ts +12 -6
- expr_tracker-0.6.1/web/src/chart-layout.test.ts +57 -0
- expr_tracker-0.6.1/web/src/chart-layout.ts +57 -0
- expr_tracker-0.6.1/web/src/chart-settings-dialog.tsx +174 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-settings.test.ts +11 -1
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-settings.ts +17 -5
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-ticks.test.ts +18 -1
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-ticks.ts +14 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/dialog.tsx +10 -3
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/input.tsx +1 -1
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/select.tsx +5 -5
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/sheet.tsx +1 -1
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/curve-data.test.ts +8 -3
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/curve-data.ts +6 -7
- expr_tracker-0.6.1/web/src/curve-groups.tsx +265 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/curve-worker.ts +3 -2
- expr_tracker-0.6.1/web/src/metric-chart.tsx +363 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/run-picker.tsx +2 -2
- expr_tracker-0.6.1/web/src/runs-panel.tsx +136 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/session.tsx +1 -1
- expr_tracker-0.6.1/web/src/source-aggregation.test.ts +22 -0
- expr_tracker-0.6.1/web/src/source-aggregation.ts +49 -0
- expr_tracker-0.6.1/web/src/styles.css +240 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/theme.tsx +1 -0
- expr_tracker-0.6.1/web/src/use-chart-drag.ts +101 -0
- expr_tracker-0.6.1/web/src/workspace.test.ts +46 -0
- expr_tracker-0.6.1/web/src/workspace.ts +101 -0
- expr_tracker-0.5.0/src/expr_tracker/_web/assets/curve-worker-BkxJLhdR.js +0 -2
- expr_tracker-0.5.0/src/expr_tracker/_web/assets/index-Cl3CI7bq.css +0 -1
- expr_tracker-0.5.0/src/expr_tracker/_web/assets/index-Czg8NFJT.js +0 -53
- expr_tracker-0.5.0/web/e2e/viewer.spec.ts +0 -698
- expr_tracker-0.5.0/web/src/app.tsx +0 -236
- expr_tracker-0.5.0/web/src/chart-settings-dialog.tsx +0 -83
- expr_tracker-0.5.0/web/src/curve-groups.tsx +0 -135
- expr_tracker-0.5.0/web/src/metric-chart.tsx +0 -206
- expr_tracker-0.5.0/web/src/styles.css +0 -134
- expr_tracker-0.5.0/web/src/workspace-storage.test.ts +0 -47
- expr_tracker-0.5.0/web/src/workspace-storage.ts +0 -54
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/.github/workflows/ci.yaml +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/.github/workflows/docs.yaml +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/.github/workflows/release.yaml +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/.gitignore +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/LICENSE +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/compare_native.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/native_workloads.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/profile_logging.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/architecture-simplification.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/artifact-atime-isolation.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/artifact-metadata-investigation.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/file-splitting.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/logging-100-metrics.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/native-migration.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/native-optimized.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/notification-simplification.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/observer-projection.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/serializer-cache-probe.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/series-cache-probe.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/transport-optimized.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/validation-reuse.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/viewer-lock-scope.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/viewer.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/architecture.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/benchmarks/native-migration.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/design.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/examples.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/getting-started.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/alerts.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/artifacts.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/backends.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/cli.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/distributed.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/history.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/logging.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/spans.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/streams.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/index.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/native-development.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/reference/api.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/reference/configuration.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/reference/expressions.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/README.md +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/alert_rules.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/checkpoints.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/early_stopping.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/multiprocess_pipeline.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/profile_step.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/quickstart.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/mkdocs.yml +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts/delivery.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts/evaluation.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts/rules.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts/watchdog.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts_parser.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts_plan.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/artifacts.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/json.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/notifications.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/reader/segments.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/reader.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/record.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/series.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/state/operations.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/state.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/tests.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/writer/persistence.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/writer/tests.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/writer.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/lib.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/spans.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/summary.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/trace/lanes.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/trace.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/auth.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/directories.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/identity.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer_json.rs +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/__init__.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_compat.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_process.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/__init__.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/backends/__init__.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/backends/base.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/backends/cards.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/dispatch.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/engine.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/__init__.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/eval.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/functions.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/lexer.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/nodes.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/parser.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/rule.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/models.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/artifacts.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/cli.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/encoders.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/__init__.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/_store_types.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/_store_writes.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/codec.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/frame.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/naming.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/reader.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/rotation.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/series.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/store.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/writer.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/plugins.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/py.typed +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/run.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/spans.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/summary.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/trace.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/tracker.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/types.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_artifact_metadata.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_callback_helpers.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_reader_edge_helpers.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_span_helpers.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_span_plugin_helpers.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_trace_helpers.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/conftest.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_backends.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_delivery.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_dispatch.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_engine.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_models.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_native.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_routing.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_artifact_metadata.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_artifacts.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_benchmark.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_cache.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_callback_isolation.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_callback_lifecycle.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_cli.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_commit_timestamps.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_correctness.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_distributed.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_e2e.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_examples.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_builder.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_eval.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_functions.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_parser.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_properties.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_failure_modes.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_features.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_fork_safety.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_frame_codec_summary.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_history.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_history_admission.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_history_concurrency.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_hot_paths.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_integration.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_lark.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_native_commit_queue.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_native_fast_paths.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_native_metric_projection.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_nonfinite_conditions.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_perf.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_plugin_cpu_time.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_plugin_gpu_stats.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_plugin_resources.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_plugin_torch_memory.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_process_compat.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_public_surfaces.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_reader_edge_cases.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_reader_live_queries.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_review_regressions.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_rotation.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_rule_lifecycle.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_rule_overrides.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_rule_quoted_names.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_run_backends.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_run_dir.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_scenarios.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_serialization_edge_cases.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_series_edge_cases.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_source_size.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_coordination.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_lifecycle.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_plugins.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_printing.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_value_encoding.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_spans.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_step_commit.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_step_types.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_streams.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_stress.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trace.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trace_export_failures.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trace_perf.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trace_queries.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trackio.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_unicode_records.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_value_encoding.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_wandb.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_async.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_buffer.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_bytes.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_diagnostics.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_durability.py +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/components.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/playwright.config.ts +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/badge.tsx +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/button.tsx +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/card.tsx +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/checkbox.tsx +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/switch.tsx +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/tabs.tsx +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/curve-stream.ts +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/inspector.tsx +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/lib/utils.ts +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/main.tsx +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/plot-scheduler.test.ts +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/plot-scheduler.ts +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/tsconfig.json +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/vite.config.ts +0 -0
- {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/vitest.config.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: expr_tracker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -100,7 +100,7 @@ and view config, summary, and spans across rotation segments.
|
|
|
100
100
|
Enter the random key printed at startup in the viewer's login form. The default listener is
|
|
101
101
|
`0.0.0.0`; use a trusted network or HTTPS/SSH for remote access.
|
|
102
102
|
Use **Add run** to select each run's directory on the server.
|
|
103
|
-
Added runs are
|
|
103
|
+
Added runs are saved on the server and shared across browsers; subdirectories are not scanned for runs.
|
|
104
104
|
See the [viewer guide](docs/guide/web-viewer.md) and
|
|
105
105
|
[native build instructions](docs/native-development.md).
|
|
106
106
|
|
|
@@ -50,7 +50,7 @@ and view config, summary, and spans across rotation segments.
|
|
|
50
50
|
Enter the random key printed at startup in the viewer's login form. The default listener is
|
|
51
51
|
`0.0.0.0`; use a trusted network or HTTPS/SSH for remote access.
|
|
52
52
|
Use **Add run** to select each run's directory on the server.
|
|
53
|
-
Added runs are
|
|
53
|
+
Added runs are saved on the server and shared across browsers; subdirectories are not scanned for runs.
|
|
54
54
|
See the [viewer guide](docs/guide/web-viewer.md) and
|
|
55
55
|
[native build instructions](docs/native-development.md).
|
|
56
56
|
|
|
@@ -18,13 +18,13 @@ through HTTPS). Keys are not stored in browser local storage or URLs.
|
|
|
18
18
|
Login is remembered for 30 days, including browser and server restarts.
|
|
19
19
|
**Sign out** revokes the session and clears its cookie. Scripted clients can still use HTTP Basic
|
|
20
20
|
with username `et` and the startup key. Previously added runs are restored after login.
|
|
21
|
-
`--root`
|
|
21
|
+
`--root` sets the directory picker's initial location until a shared location is saved. Use `--port` to
|
|
22
22
|
choose another local port. Add up to six runs to the workspace and use their
|
|
23
23
|
checkboxes to compare them. Removing a run from the sidebar only removes it
|
|
24
24
|
from the workspace, not from disk. Each run has its own stream/rank selector;
|
|
25
25
|
sources are never silently merged.
|
|
26
26
|
|
|
27
|
-
`et serve` stores the key
|
|
27
|
+
`et serve` stores the key, active sessions, and shared workspace in
|
|
28
28
|
`$XDG_STATE_HOME/expr_tracker/viewer-auth-<port>.json`, or
|
|
29
29
|
`~/.local/state/expr_tracker/viewer-auth-<port>.json` when that variable is unset
|
|
30
30
|
or relative. The file is private (`0600` on Unix), atomically updated, and locked
|
|
@@ -44,7 +44,7 @@ Display names come from `tracker.name` and `tracker.project` in the run's config
|
|
|
44
44
|
not necessarily the directory basename. The viewer prefers `config.json`, then
|
|
45
45
|
`config.trainer-r0.json`, then the first stream config. This optional identity
|
|
46
46
|
read is size-limited and cached until the chosen config changes; missing or
|
|
47
|
-
invalid identity metadata falls back to directory names. Saved
|
|
47
|
+
invalid identity metadata falls back to directory names. Saved workspace entries
|
|
48
48
|
refresh their display names automatically.
|
|
49
49
|
The directory path is shown in the picker's editable path field.
|
|
50
50
|
This is not a browser upload picker: with SSH forwarding, paths
|
|
@@ -52,30 +52,72 @@ refer to the remote training host, not your laptop. Relative paths are resolved
|
|
|
52
52
|
against the startup `--root`; absolute paths can select any directory accessible
|
|
53
53
|
to the server process, including directories outside the startup root.
|
|
54
54
|
|
|
55
|
-
Added runs, their selected sources, colors,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
Added runs, their selected sources, colors, checkbox states, and the picker's
|
|
56
|
+
last added directory are saved on the server, alongside its private authentication
|
|
57
|
+
state. All browsers signed in to the same viewer share this workspace; visible
|
|
58
|
+
pages check for changes every three seconds, even when **Live** is off, and also
|
|
59
|
+
when focused. Reloading or restarting the server retains the saved workspace.
|
|
60
|
+
Concurrent edits are revision-checked: a stale edit is rejected and the latest
|
|
61
|
+
workspace is reloaded instead of silently overwriting another browser's changes.
|
|
62
|
+
Loading or saving failures display an error and retry control; failed saves do
|
|
63
|
+
not replace the last confirmed state. Removing a run only updates the shared
|
|
64
|
+
list, not the experiment files. Unavailable saved directories remain listed
|
|
65
|
+
with errors rather than being silently discarded.
|
|
66
|
+
|
|
67
|
+
Run directories are no longer read from or written to browser local storage.
|
|
68
|
+
Legacy browser-only lists are not automatically published to the shared server;
|
|
69
|
+
add those directories once to share them. Theme and per-chart preferences remain
|
|
70
|
+
local to each browser. No metric data or experiment config contents are persisted
|
|
71
|
+
in the shared workspace. Live and manual refresh only reload explicitly added
|
|
72
|
+
run directories. A `?root=...` bookmark can override the picker's starting location
|
|
73
|
+
for that page until a run is added.
|
|
74
|
+
|
|
75
|
+
Experiment files are never uploaded, modified, or deleted. Browsing directories
|
|
65
76
|
with more than 10,000 entries fails explicitly; enter a child directory path
|
|
66
77
|
directly instead.
|
|
67
78
|
|
|
68
79
|
## Curve comparison
|
|
69
80
|
|
|
70
|
-
The React + shadcn/ui workspace
|
|
81
|
+
The React + shadcn/ui workspace arranges independent rounded panels on a shared
|
|
82
|
+
canvas. The Runs panel contains the application icon and name, run search, and
|
|
83
|
+
the run list. The plus icon beside run search adds a directory. Its bottom row
|
|
84
|
+
groups the **Live** toggle button, **Refresh**, and **Settings**;
|
|
85
|
+
settings contains theme switching and sign out. On desktop, drag the panel's
|
|
86
|
+
right edge to resize its width without changing its height. The focused edge
|
|
87
|
+
also accepts Left/Right (Shift for larger changes), Home to reset, and Escape to cancel a drag.
|
|
88
|
+
The button beside **Experiment tracker** collapses the desktop panel into a narrow
|
|
89
|
+
rail. The application icon becomes the expand button, followed by Add and each
|
|
90
|
+
run's colored checkbox; hover or focus a checkbox to see its run name.
|
|
91
|
+
The rail shows all runs even when the expanded list is filtered, and keeps Live,
|
|
92
|
+
Refresh, and Settings available. Expand again to restore the previous width and
|
|
93
|
+
search without changing selection or reloading charts.
|
|
94
|
+
Desktop collapse and mobile management expansion are saved independently in this
|
|
95
|
+
browser and restored on reload. A warning appears if browser storage is unavailable.
|
|
96
|
+
Chart panels resize with the available space without reloading their data.
|
|
97
|
+
Source, details, and remove actions appear when a run is hovered or keyboard
|
|
98
|
+
focused, remain visible while its source menu is open, and are always available
|
|
99
|
+
on touch screens. Narrow screens use wider canvas margins and a compact Runs panel:
|
|
100
|
+
scroll the named run chips horizontally to select runs, or expand management for
|
|
101
|
+
source, details, and remove actions. Add, Live, Refresh, and Settings remain available.
|
|
102
|
+
Run and metric search fields are hidden on narrow screens, and `/` does not focus search.
|
|
103
|
+
Chart pin, settings, and expand actions likewise appear on hover or keyboard focus,
|
|
104
|
+
and stay visible on touch screens. The resize grip appears only when hovered,
|
|
105
|
+
keyboard focused, or dragged; search fields use a thin inset focus indicator.
|
|
106
|
+
Dropdown menus share the panels' compact radii, surface colors, and subtle borders.
|
|
107
|
+
|
|
108
|
+
The workspace uses single-line run rows. Metrics are grouped
|
|
71
109
|
by the first segment of their name:
|
|
72
110
|
`train/loss` and `train/optimizer/lr` appear under **train**; names without a
|
|
73
|
-
namespace appear under **Other**. Each section
|
|
74
|
-
|
|
111
|
+
namespace appear under **Other**. Each section has independent **Columns** and
|
|
112
|
+
**Rows** dropdowns, both accepting 1–5 (default 4 columns and 2 rows).
|
|
113
|
+
Page capacity is columns times rows, so the default desktop
|
|
114
|
+
page shows up to eight charts in two rows. Left/right arrows navigate pages.
|
|
115
|
+
Narrow screens use one column and the same configured row count.
|
|
116
|
+
Search or a layout change resets pagination to the first matching
|
|
75
117
|
page; groups can also be collapsed. Only the current page is queried and rendered.
|
|
76
118
|
The sidebar contains
|
|
77
119
|
only runs; each row provides source selection, details, and removal controls.
|
|
78
|
-
Search
|
|
120
|
+
Search and axis share one rounded sticky toolbar panel (horizontally
|
|
79
121
|
scrollable on narrow screens). Search filters charts immediately, matching full metric
|
|
80
122
|
names case-insensitively (multiple words must all match). Press `/` to focus
|
|
81
123
|
search and Escape to clear it. The header theme button switches between light
|
|
@@ -84,9 +126,31 @@ and dark themes, including chart axes and series colors, and saves the preferenc
|
|
|
84
126
|
Each uPlot chart overlays the selected runs with consistent colors, with its
|
|
85
127
|
legend centered above the plot. Values and the hovered step/time appear only in
|
|
86
128
|
that chart's hover tooltip; cursors are not synchronized across charts.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
Each series shows its actual nearest sample, never an interpolated value.
|
|
130
|
+
Ties choose the left sample. The matched sample is highlighted with a larger
|
|
131
|
+
marker, and the tooltip heading snaps to an actual sampled step/time.
|
|
132
|
+
Use **Step**, **Wall time**, or **Relative time** to change alignment.
|
|
133
|
+
Plots show only the left and bottom axis borders, with short outward ticks and
|
|
134
|
+
horizontal grid lines. Y-axis space fits its labels to avoid excessive left
|
|
135
|
+
padding. The X axis includes the first and last plotted
|
|
136
|
+
data values, suppressing nearby regular ticks; endpoint labels are stacked when
|
|
137
|
+
needed to remain readable in narrow charts. These details also appear in PNG exports.
|
|
138
|
+
Relative time is elapsed time since the selected source's earliest valid
|
|
139
|
+
timestamp, independently for each run, displayed in seconds, minutes, hours, or
|
|
140
|
+
days. Filtering steps or downsampling does not reset that origin; records without
|
|
141
|
+
valid timestamps are skipped with a warning.
|
|
142
|
+
Each chart's pin button adds a copy to a separate **Pinned** section above
|
|
143
|
+
the namespace groups, keeping the chart in its original section. Unpinning
|
|
144
|
+
only removes the pinned copy. Both copies share the metric's chart settings.
|
|
145
|
+
Pinned charts support the same rows, paging, filtering, and focus controls.
|
|
146
|
+
Hover a chart title to reveal its grab handle, then drag it before or after
|
|
147
|
+
another chart in the same section. Hold within 48 pixels of either section edge
|
|
148
|
+
for 650 ms to turn a page while dragging; release over a chart to place it.
|
|
149
|
+
Escape cancels a drag. Focused titles also support arrow-key reordering:
|
|
150
|
+
left/right move one position, up/down move one row. Pinned and original sections
|
|
151
|
+
have independent ordering. Pins, ordering, and each section's column and row counts are remembered in the current browser, independently of
|
|
152
|
+
the server-synchronized run directories.
|
|
153
|
+
Each chart can expand into a focused full-width view. On narrow screens,
|
|
90
154
|
the sidebar controls collapse so curves remain the primary content.
|
|
91
155
|
|
|
92
156
|
### Per-chart settings
|
|
@@ -109,6 +173,41 @@ changes do not reread logs or alter recorded values. Hover tooltips show the
|
|
|
109
173
|
displayed values and identify active EMA smoothing. **Reset** restores the
|
|
110
174
|
default controls; apply them to save the reset.
|
|
111
175
|
|
|
176
|
+
### Combining sources
|
|
177
|
+
|
|
178
|
+
Open an existing chart's settings and change **Source mode** from **Selected source**
|
|
179
|
+
to **Overlay**, **Sum**, **Mean**, **Maximum**, or **Minimum**. The chart always
|
|
180
|
+
shows the same metric; aggregation combines its values across source files within
|
|
181
|
+
each experiment, never different metrics or different experiments.
|
|
182
|
+
Choose sources with the checkboxes, or enter a wildcard under **Source pattern**:
|
|
183
|
+
`rollout-*` selects `metrics.rollout-0.jsonl` through `metrics.rollout-7.jsonl`;
|
|
184
|
+
`trainer-*` selects trainer sources. Full filenames such as
|
|
185
|
+
`metrics.rollout-*.jsonl` also work. `*` matches any text and `?` matches one
|
|
186
|
+
character. A nonempty pattern replaces the manual selection and automatically
|
|
187
|
+
matches new available sources. Up to 128 inputs
|
|
188
|
+
are supported; an empty or excessive match is reported explicitly.
|
|
189
|
+
|
|
190
|
+
Overlay draws the selected sources as separate curves. Source colors distinguish
|
|
191
|
+
sources, and different run line styles distinguish runs; legends and tooltips
|
|
192
|
+
include both names. Reducers produce one curve per run, combining the selected
|
|
193
|
+
sources in the same recorded step. Runs are never aggregated together.
|
|
194
|
+
Only finite values present at that step participate: missing values are not
|
|
195
|
+
interpolated, forward-filled, or replaced with zero, and an entirely missing step
|
|
196
|
+
produces no sample. Means divide by the number of present values.
|
|
197
|
+
Reduction happens on the server before downsampling, EMA smoothing, and log filtering.
|
|
198
|
+
Time axes use the earliest selected-source timestamp at each step. Relative time
|
|
199
|
+
starts at the earliest timestamp across the selected sources' entire histories,
|
|
200
|
+
independently of metric presence, query window, and downsampling.
|
|
201
|
+
Numeric overflow is reported and omitted.
|
|
202
|
+
|
|
203
|
+
The original metric remains the chart's title and position; its subtitle identifies
|
|
204
|
+
the combination mode and input count. Other charts and source logs are unchanged.
|
|
205
|
+
Choices are saved with that chart's browser-local settings, shared by its pinned
|
|
206
|
+
copy, and included in PNG exports. Source aggregation does not change the run's
|
|
207
|
+
selected source in the sidebar. Cached requests are reused when returning to the
|
|
208
|
+
same operation and inputs. Legacy metric-aggregation settings are removed on load,
|
|
209
|
+
while smoothing, axes, height, and other appearance settings are preserved.
|
|
210
|
+
|
|
112
211
|
### Records and streaming
|
|
113
212
|
|
|
114
213
|
Each run's **Details** button opens config/summary, paginated records, and spans
|
|
@@ -132,7 +231,8 @@ There is no workspace-wide metric limit. A Web Worker streams history over
|
|
|
132
231
|
`/api/history/stream` as NDJSON and prepares finite per-run x/y arrays
|
|
133
232
|
off the UI thread. Transferable `Float64Array` buffers move to uPlot's shared-axis,
|
|
134
233
|
per-series data mode without cloning thousands of boxed numbers on the UI thread.
|
|
135
|
-
Queries contain
|
|
234
|
+
Queries contain only metrics on the current section page (up to 20 with four
|
|
235
|
+
columns and five rows) and share two concurrent
|
|
136
236
|
streams. Each native chunk contains at most 256 points, with a bounded two-chunk
|
|
137
237
|
channel providing backpressure. Disconnecting cancels the scan.
|
|
138
238
|
|
|
@@ -141,6 +241,10 @@ row (including 5,000+ points); larger ranges use extrema-preserving downsampling
|
|
|
141
241
|
Sections initialize when approached. Scrolling away pauses subscriptions, drawing,
|
|
142
242
|
after a short grace period; initialized canvases stay
|
|
143
243
|
mounted so scrolling back does not repeatedly destroy and recreate plots.
|
|
244
|
+
Completed views with unchanged request and transform keys reuse their current
|
|
245
|
+
data without resubscribing or transferring worker snapshots. Canvas drawing is
|
|
246
|
+
skipped unless data, dimensions, theme, or chart settings change. Changed source
|
|
247
|
+
metadata and manual refresh still invalidate offscreen views when revisited.
|
|
144
248
|
Hover values update lightweight DOM labels rather than rerendering React charts.
|
|
145
249
|
Shared worker streams avoid
|
|
146
250
|
duplicate scans for neighboring charts, and completed sources are cached with
|
|
@@ -161,7 +265,8 @@ workspace or refetching successful sources.
|
|
|
161
265
|
|
|
162
266
|
Live is off by default. When enabled, it polls metadata every three seconds while the page is visible, fetching new
|
|
163
267
|
curves only when source metadata changes. Visible charts update in place; old
|
|
164
|
-
data remains visible
|
|
268
|
+
data remains visible during refresh. Each loading chart's border pulses instead
|
|
269
|
+
of displaying a spinner; reduced-motion mode uses a steady border. Manual **Refresh**
|
|
165
270
|
also invalidates curve queries. It refreshes selected runs, not unselected
|
|
166
271
|
sidebar entries, and sees only complete rows already written to disk. It cannot display
|
|
167
272
|
an open metric row or records still held in the training process's write buffer.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "expr_tracker"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.1"
|
|
4
4
|
description = "Local-first experiment tracking with queryable history and expression-based alerts on your training metrics"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -97,6 +97,7 @@ include = [
|
|
|
97
97
|
{ path = "web/package-lock.json", format = "sdist" },
|
|
98
98
|
{ path = "web/components.json", format = "sdist" },
|
|
99
99
|
{ path = "web/index.html", format = "sdist" },
|
|
100
|
+
{ path = "web/public/**/*", format = "sdist" },
|
|
100
101
|
{ path = "web/tsconfig.json", format = "sdist" },
|
|
101
102
|
{ path = "web/vite.config.ts", format = "sdist" },
|
|
102
103
|
{ path = "web/vitest.config.ts", format = "sdist" },
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
#[derive(Default)]
|
|
4
|
+
struct Reduction {
|
|
5
|
+
count: usize,
|
|
6
|
+
scale: f64,
|
|
7
|
+
sum: f64,
|
|
8
|
+
min: f64,
|
|
9
|
+
max: f64,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
impl Reduction {
|
|
13
|
+
fn push(&mut self, value: f64) {
|
|
14
|
+
if self.count == 0 {
|
|
15
|
+
self.min = value;
|
|
16
|
+
self.max = value;
|
|
17
|
+
} else {
|
|
18
|
+
self.min = self.min.min(value);
|
|
19
|
+
self.max = self.max.max(value);
|
|
20
|
+
}
|
|
21
|
+
let scale = value.abs();
|
|
22
|
+
if scale > self.scale {
|
|
23
|
+
self.sum *= self.scale / scale;
|
|
24
|
+
self.scale = scale;
|
|
25
|
+
}
|
|
26
|
+
if self.scale > 0.0 {
|
|
27
|
+
self.sum += value / self.scale;
|
|
28
|
+
}
|
|
29
|
+
self.count += 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn value(&self, mode: &str) -> Option<f64> {
|
|
33
|
+
if self.count == 0 {
|
|
34
|
+
return None;
|
|
35
|
+
}
|
|
36
|
+
Some(match mode {
|
|
37
|
+
"min" => self.min,
|
|
38
|
+
"max" => self.max,
|
|
39
|
+
"mean" => (self.sum / self.count as f64) * self.scale,
|
|
40
|
+
_ => self.sum * self.scale,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub(super) fn sources(
|
|
46
|
+
viewer: &Viewer,
|
|
47
|
+
run: &Run,
|
|
48
|
+
query: &HistoryQuery,
|
|
49
|
+
cancelled: impl Fn() -> bool,
|
|
50
|
+
) -> Result<CachedLog> {
|
|
51
|
+
let files: Vec<String> = serde_json::from_str(
|
|
52
|
+
query
|
|
53
|
+
.files
|
|
54
|
+
.as_deref()
|
|
55
|
+
.ok_or_else(|| invalid("Source aggregation requires files"))?,
|
|
56
|
+
)?;
|
|
57
|
+
let mode = query.aggregation.as_deref().unwrap_or("");
|
|
58
|
+
if query.file.is_some() || !["sum", "mean", "max", "min"].contains(&mode) {
|
|
59
|
+
return Err(invalid(
|
|
60
|
+
"Choose sum, mean, max or min with files, not a single file",
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
if files.is_empty() || files.len() > 128 {
|
|
64
|
+
return Err(invalid("Select between 1 and 128 metric sources per run"));
|
|
65
|
+
}
|
|
66
|
+
let paths = files
|
|
67
|
+
.iter()
|
|
68
|
+
.map(|file| {
|
|
69
|
+
let (path, spans) = viewer.file(run, Some(file))?;
|
|
70
|
+
if spans {
|
|
71
|
+
return Err(invalid("Aggregation requires metric sources"));
|
|
72
|
+
}
|
|
73
|
+
Ok(path)
|
|
74
|
+
})
|
|
75
|
+
.collect::<Result<Vec<_>>>()?;
|
|
76
|
+
if paths.iter().collect::<BTreeSet<_>>().len() != paths.len() {
|
|
77
|
+
return Err(invalid("Duplicate aggregation sources"));
|
|
78
|
+
}
|
|
79
|
+
let metrics: Vec<String> = serde_json::from_str(&query.metrics)?;
|
|
80
|
+
if metrics.is_empty() || metrics.len() > 128 || metrics.iter().any(|metric| metric.len() > 4096)
|
|
81
|
+
{
|
|
82
|
+
return Err(invalid("Select between 1 and 128 metrics"));
|
|
83
|
+
}
|
|
84
|
+
let mut output = CachedLog::new(viewer.stopping.clone())?;
|
|
85
|
+
output
|
|
86
|
+
.db
|
|
87
|
+
.execute_batch("CREATE TABLE source_rows(step TEXT, sort TEXT, time REAL, data TEXT);")?;
|
|
88
|
+
let transaction = output.db.transaction()?;
|
|
89
|
+
let mut missing_sources = 0;
|
|
90
|
+
{
|
|
91
|
+
let mut insert = transaction.prepare("INSERT INTO source_rows VALUES (?1,?2,?3,?4)")?;
|
|
92
|
+
for path in paths {
|
|
93
|
+
viewer.with_log(&path, false, |log| {
|
|
94
|
+
output.corrupt += log.corrupt;
|
|
95
|
+
output.surrogates |= log.surrogates;
|
|
96
|
+
if !metrics.iter().any(|metric| log.metrics.contains(metric)) {
|
|
97
|
+
missing_sources += 1;
|
|
98
|
+
}
|
|
99
|
+
let mut statement = log.db.prepare("SELECT step,sort,time,data FROM records")?;
|
|
100
|
+
let mut rows = statement.query([])?;
|
|
101
|
+
while let Some(row) = rows.next()? {
|
|
102
|
+
if cancelled() {
|
|
103
|
+
return Err(invalid("Aggregation disconnected"));
|
|
104
|
+
}
|
|
105
|
+
let raw: String = row.get(3)?;
|
|
106
|
+
let value: Value = serde_json::from_str(&raw)?;
|
|
107
|
+
let values: Map<String, Value> = metrics
|
|
108
|
+
.iter()
|
|
109
|
+
.filter_map(|metric| {
|
|
110
|
+
value
|
|
111
|
+
.get(metric)
|
|
112
|
+
.map(|value| (metric.clone(), value.clone()))
|
|
113
|
+
})
|
|
114
|
+
.collect();
|
|
115
|
+
insert.execute(params![
|
|
116
|
+
row.get::<_, String>(0)?,
|
|
117
|
+
row.get::<_, String>(1)?,
|
|
118
|
+
row.get::<_, Option<f64>>(2)?,
|
|
119
|
+
serde_json::to_string(&values)?
|
|
120
|
+
])?;
|
|
121
|
+
}
|
|
122
|
+
Ok(())
|
|
123
|
+
})?;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let mut overflow = 0;
|
|
127
|
+
let mut nonfinite = 0;
|
|
128
|
+
{
|
|
129
|
+
let mut statement =
|
|
130
|
+
transaction.prepare("SELECT step,sort,time,data FROM source_rows ORDER BY sort")?;
|
|
131
|
+
let mut rows = statement.query([])?;
|
|
132
|
+
let mut insert = transaction
|
|
133
|
+
.prepare("INSERT INTO records(step,sort,ordinal,time,data) VALUES (?1,?2,?3,?4,?5)")?;
|
|
134
|
+
let mut current: Option<(String, String)> = None;
|
|
135
|
+
let mut time: Option<f64> = None;
|
|
136
|
+
let mut values: Vec<Reduction> = (0..metrics.len()).map(|_| Reduction::default()).collect();
|
|
137
|
+
let mut ordinal = 0i64;
|
|
138
|
+
let mut flush =
|
|
139
|
+
|step: &str, sort: &str, time: Option<f64>, values: &[Reduction]| -> Result<()> {
|
|
140
|
+
let mut data = Map::new();
|
|
141
|
+
data.insert("_step".into(), serde_json::from_str(step)?);
|
|
142
|
+
if let Some(time) = time {
|
|
143
|
+
data.insert("_time".into(), Value::from(time));
|
|
144
|
+
}
|
|
145
|
+
for (metric, reduction) in metrics.iter().zip(values) {
|
|
146
|
+
if let Some(value) = reduction.value(mode) {
|
|
147
|
+
if value.is_finite() {
|
|
148
|
+
data.insert(metric.clone(), Value::from(value));
|
|
149
|
+
} else {
|
|
150
|
+
overflow += 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
insert.execute(params![
|
|
155
|
+
step,
|
|
156
|
+
sort,
|
|
157
|
+
ordinal,
|
|
158
|
+
time,
|
|
159
|
+
serde_json::to_string(&data)?
|
|
160
|
+
])?;
|
|
161
|
+
ordinal += 1;
|
|
162
|
+
Ok(())
|
|
163
|
+
};
|
|
164
|
+
while let Some(row) = rows.next()? {
|
|
165
|
+
if cancelled() {
|
|
166
|
+
return Err(invalid("Aggregation disconnected"));
|
|
167
|
+
}
|
|
168
|
+
let step: String = row.get(0)?;
|
|
169
|
+
let sort: String = row.get(1)?;
|
|
170
|
+
if current
|
|
171
|
+
.as_ref()
|
|
172
|
+
.is_some_and(|(previous, _)| previous != &step)
|
|
173
|
+
{
|
|
174
|
+
let (previous, previous_sort) = current.as_ref().unwrap();
|
|
175
|
+
flush(previous, previous_sort, time, &values)?;
|
|
176
|
+
values = (0..metrics.len()).map(|_| Reduction::default()).collect();
|
|
177
|
+
time = None;
|
|
178
|
+
}
|
|
179
|
+
current = Some((step, sort));
|
|
180
|
+
if let Some(timestamp) = row.get::<_, Option<f64>>(2)? {
|
|
181
|
+
time = Some(time.map_or(timestamp, |previous| previous.min(timestamp)));
|
|
182
|
+
}
|
|
183
|
+
let raw: String = row.get(3)?;
|
|
184
|
+
let data: Value = serde_json::from_str(&raw)?;
|
|
185
|
+
for (metric, reduction) in metrics.iter().zip(&mut values) {
|
|
186
|
+
if let Some(value) = data.get(metric) {
|
|
187
|
+
if let Some(value) = finite_number(value) {
|
|
188
|
+
reduction.push(value);
|
|
189
|
+
} else if value.get("$et.nonfinite").is_some() || value.is_number() {
|
|
190
|
+
nonfinite += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if let Some((step, sort)) = current {
|
|
196
|
+
flush(&step, &sort, time, &values)?;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
transaction.execute("DROP TABLE source_rows", [])?;
|
|
200
|
+
transaction.commit()?;
|
|
201
|
+
if missing_sources > 0 {
|
|
202
|
+
output.extra_warnings.push(format!(
|
|
203
|
+
"{missing_sources} selected sources have no recorded values for the requested metrics; missing values are ignored."
|
|
204
|
+
));
|
|
205
|
+
}
|
|
206
|
+
if overflow > 0 {
|
|
207
|
+
output.extra_warnings.push(format!(
|
|
208
|
+
"{overflow} aggregated values exceeded the numeric range and were omitted."
|
|
209
|
+
));
|
|
210
|
+
}
|
|
211
|
+
if nonfinite > 0 {
|
|
212
|
+
output.extra_warnings.push(format!(
|
|
213
|
+
"{nonfinite} non-finite source observations ignored."
|
|
214
|
+
));
|
|
215
|
+
}
|
|
216
|
+
Ok(output)
|
|
217
|
+
}
|
|
@@ -16,6 +16,8 @@ struct Saved {
|
|
|
16
16
|
version: u8,
|
|
17
17
|
key: String,
|
|
18
18
|
sessions: Vec<Session>,
|
|
19
|
+
#[serde(default)]
|
|
20
|
+
workspace: workspace::Workspace,
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
pub(super) struct AuthStore {
|
|
@@ -49,6 +51,7 @@ impl AuthStore {
|
|
|
49
51
|
version: 1,
|
|
50
52
|
key,
|
|
51
53
|
sessions: Vec::new(),
|
|
54
|
+
workspace: workspace::Workspace::default(),
|
|
52
55
|
};
|
|
53
56
|
let mut lock = None;
|
|
54
57
|
let path = path
|
|
@@ -115,6 +118,7 @@ impl AuthStore {
|
|
|
115
118
|
{
|
|
116
119
|
return Err(invalid_state());
|
|
117
120
|
}
|
|
121
|
+
saved.workspace.validate().map_err(|_| invalid_state())?;
|
|
118
122
|
let store = Self {
|
|
119
123
|
data: Mutex::new(saved),
|
|
120
124
|
path,
|
|
@@ -144,6 +148,29 @@ impl AuthStore {
|
|
|
144
148
|
self.data.lock().key.clone()
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
pub(super) fn workspace(&self) -> workspace::Workspace {
|
|
152
|
+
self.data.lock().workspace.clone()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pub(super) fn save_workspace(&self, mut workspace: workspace::Workspace) -> Result<workspace::Workspace> {
|
|
156
|
+
workspace.validate()?;
|
|
157
|
+
let mut data = self.data.lock();
|
|
158
|
+
if workspace.revision != data.workspace.revision {
|
|
159
|
+
return Err(Error(
|
|
160
|
+
StatusCode::CONFLICT,
|
|
161
|
+
"Workspace changed in another browser. Shared runs have been reloaded; please try again.".into(),
|
|
162
|
+
));
|
|
163
|
+
}
|
|
164
|
+
workspace.revision = workspace.revision.checked_add(1)
|
|
165
|
+
.filter(|revision| *revision < (1u64 << 53))
|
|
166
|
+
.ok_or_else(|| invalid("Workspace revision limit reached"))?;
|
|
167
|
+
let mut next = data.clone();
|
|
168
|
+
next.workspace = workspace.clone();
|
|
169
|
+
self.persist(&next)?;
|
|
170
|
+
*data = next;
|
|
171
|
+
Ok(workspace)
|
|
172
|
+
}
|
|
173
|
+
|
|
147
174
|
pub(super) fn contains_at(&self, token: &str, now: u64) -> bool {
|
|
148
175
|
self.data.lock().sessions.iter().any(|session| {
|
|
149
176
|
session.expires > now && auth::matches(token.as_bytes(), session.token.as_bytes())
|
|
@@ -21,6 +21,7 @@ pub(super) struct CachedLog {
|
|
|
21
21
|
pub(super) metrics: BTreeSet<String>,
|
|
22
22
|
pub(super) corrupt: u64,
|
|
23
23
|
pub(super) surrogates: bool,
|
|
24
|
+
pub(super) extra_warnings: Vec<String>,
|
|
24
25
|
pub(super) stopping: Arc<AtomicBool>,
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -48,12 +49,13 @@ impl CachedLog {
|
|
|
48
49
|
metrics: BTreeSet::new(),
|
|
49
50
|
corrupt: 0,
|
|
50
51
|
surrogates: false,
|
|
52
|
+
extra_warnings: Vec::new(),
|
|
51
53
|
stopping,
|
|
52
54
|
})
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
pub(super) fn warnings(&self) -> Vec<String> {
|
|
56
|
-
let mut warnings =
|
|
58
|
+
let mut warnings = self.extra_warnings.clone();
|
|
57
59
|
if self.corrupt > 0 {
|
|
58
60
|
warnings.push(format!("Skipped {} malformed records.", self.corrupt));
|
|
59
61
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
use super::LogFile;
|
|
2
|
+
|
|
3
|
+
pub(super) fn parse(name: &str) -> Option<LogFile> {
|
|
4
|
+
let stem = name.strip_suffix(".jsonl")?;
|
|
5
|
+
let mut parts: Vec<&str> = stem.split('.').collect();
|
|
6
|
+
let kind = parts.first()?;
|
|
7
|
+
if !["metrics", "spans"].contains(kind) {
|
|
8
|
+
return None;
|
|
9
|
+
}
|
|
10
|
+
let mut rank = 0;
|
|
11
|
+
if parts.len() > 1 {
|
|
12
|
+
if let Some(number) = parts
|
|
13
|
+
.last()?
|
|
14
|
+
.strip_prefix("rank")
|
|
15
|
+
.and_then(|rank| rank.parse::<u64>().ok())
|
|
16
|
+
{
|
|
17
|
+
rank = number;
|
|
18
|
+
parts.pop();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if parts.len() > 2 {
|
|
22
|
+
return None;
|
|
23
|
+
}
|
|
24
|
+
Some(LogFile {
|
|
25
|
+
name: name.into(),
|
|
26
|
+
kind: parts[0].into(),
|
|
27
|
+
stream: parts.get(1).map(|stream| (*stream).into()),
|
|
28
|
+
rank,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
@@ -44,7 +44,9 @@ fn query(log: &CachedLog, query: &HistoryQuery, metrics: &[String], sender: &Sen
|
|
|
44
44
|
sender,
|
|
45
45
|
json!({"type":"start", "metrics":metrics, "total":total, "warnings":log.warnings()}),
|
|
46
46
|
)?;
|
|
47
|
-
let
|
|
47
|
+
let axis = query.axis.as_deref().unwrap_or("step");
|
|
48
|
+
let time = axis != "step";
|
|
49
|
+
let origin = time_origin(log, axis)?;
|
|
48
50
|
let sql = if time {
|
|
49
51
|
"SELECT data FROM records WHERE sort>=?1 AND sort<?2 ORDER BY time,sort"
|
|
50
52
|
} else {
|
|
@@ -88,7 +90,11 @@ fn query(log: &CachedLog, query: &HistoryQuery, metrics: &[String], sender: &Sen
|
|
|
88
90
|
number
|
|
89
91
|
})
|
|
90
92
|
.collect();
|
|
91
|
-
let point = Point {
|
|
93
|
+
let point = Point {
|
|
94
|
+
at,
|
|
95
|
+
x: x - origin,
|
|
96
|
+
ys,
|
|
97
|
+
};
|
|
92
98
|
if total <= limit {
|
|
93
99
|
points.push(point);
|
|
94
100
|
} else {
|
|
@@ -138,7 +144,7 @@ pub(super) async fn history(
|
|
|
138
144
|
"max_points is too small for the selected metric count",
|
|
139
145
|
));
|
|
140
146
|
}
|
|
141
|
-
if !["step", "time"].contains(&query_args.axis.as_deref().unwrap_or("step")) {
|
|
147
|
+
if !["step", "time", "relative"].contains(&query_args.axis.as_deref().unwrap_or("step")) {
|
|
142
148
|
return Err(invalid("Unknown chart axis"));
|
|
143
149
|
}
|
|
144
150
|
let low = bound(query_args.from_step.as_deref())?;
|
|
@@ -155,6 +161,10 @@ pub(super) async fn history(
|
|
|
155
161
|
let result = (|| {
|
|
156
162
|
let root = viewer.resolve_root(query_args.root.as_deref())?;
|
|
157
163
|
let run = viewer.run(&root, &query_args.run)?;
|
|
164
|
+
if query_args.files.is_some() || query_args.aggregation.is_some() {
|
|
165
|
+
let log = aggregate::sources(&viewer, &run, &query_args, || sender.is_closed())?;
|
|
166
|
+
return query(&log, &query_args, &metrics, &sender);
|
|
167
|
+
}
|
|
158
168
|
let (path, spans) = viewer.file(&run, query_args.file.as_deref())?;
|
|
159
169
|
if spans {
|
|
160
170
|
return Err(invalid("Charts require a metric source"));
|