labnotes 0.0.110__tar.gz → 0.0.112__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.
Files changed (185) hide show
  1. {labnotes-0.0.110 → labnotes-0.0.112}/PKG-INFO +1 -1
  2. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/context.md +14 -5
  3. labnotes-0.0.112/src/labnotes/is_non_negative_number.py +10 -0
  4. labnotes-0.0.112/src/labnotes/memory_interval.py +2 -0
  5. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/models.py +2 -0
  6. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/execute_run.py +18 -2
  7. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/read_experiment_config.py +16 -1
  8. labnotes-0.0.112/src/labnotes/runner/record_memory.py +18 -0
  9. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/run_experiment.py +1 -0
  10. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/open_tables.py +16 -1
  11. labnotes-0.0.112/src/labnotes/tables/read_memory.py +34 -0
  12. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/read_run.py +1 -0
  13. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/utils/process_alive.py +1 -1
  14. labnotes-0.0.112/src/labnotes/utils/tree_rss.py +6 -0
  15. labnotes-0.0.110/src/labnotes/utils/tree_rss.py → labnotes-0.0.112/src/labnotes/utils/tree_usage.py +2 -2
  16. {labnotes-0.0.110 → labnotes-0.0.112}/.gitignore +0 -0
  17. {labnotes-0.0.110 → labnotes-0.0.112}/CHANGELOG.md +0 -0
  18. {labnotes-0.0.110 → labnotes-0.0.112}/MIGRATIONS.md +0 -0
  19. {labnotes-0.0.110 → labnotes-0.0.112}/README.md +0 -0
  20. {labnotes-0.0.110 → labnotes-0.0.112}/pyproject.toml +0 -0
  21. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/__init__.py +0 -0
  22. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/__main__.py +0 -0
  23. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/asset_url.py +0 -0
  24. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/check_counts.py +0 -0
  25. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/cli.py +0 -0
  26. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/compose_experiment.py +0 -0
  27. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/context_guide.py +0 -0
  28. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/count_groups.py +0 -0
  29. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/count_runs.py +0 -0
  30. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/create_app.py +0 -0
  31. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/create_experiment.py +0 -0
  32. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/create_note.py +0 -0
  33. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/__init__.py +0 -0
  34. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/control_run.py +0 -0
  35. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/daemon_app.py +0 -0
  36. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/daemon_client.py +0 -0
  37. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/daemon_running.py +0 -0
  38. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/daemon_server.py +0 -0
  39. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/describe_status.py +0 -0
  40. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/ensure_daemon.py +0 -0
  41. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/exit_when_idle.py +0 -0
  42. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/finalize_lost_runs.py +0 -0
  43. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/remote_start.py +0 -0
  44. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/run_daemon.py +0 -0
  45. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/run_logs.py +0 -0
  46. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/serve_daemon.py +0 -0
  47. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/socket_path.py +0 -0
  48. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/stop_runs.py +0 -0
  49. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/watch_daemon.py +0 -0
  50. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon/watch_status.py +0 -0
  51. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/daemon_command.py +0 -0
  52. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/data_html.py +0 -0
  53. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/default_config_text.py +0 -0
  54. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/default_counts.py +0 -0
  55. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/drain_watch.py +0 -0
  56. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/experiment_notes.py +0 -0
  57. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/experiment_runs.py +0 -0
  58. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/experiments.py +0 -0
  59. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/file_changes.py +0 -0
  60. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/format_duration.py +0 -0
  61. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/format_started.py +0 -0
  62. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/format_value.py +0 -0
  63. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/group_runs.py +0 -0
  64. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/init.py +0 -0
  65. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/invocation_started.py +0 -0
  66. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/is_non_negative_int.py +0 -0
  67. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/is_positive_int.py +0 -0
  68. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/list_experiments.py +0 -0
  69. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/list_runs.py +0 -0
  70. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/load_config.py +0 -0
  71. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/load_experiment.py +0 -0
  72. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/load_run.py +0 -0
  73. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/log.py +0 -0
  74. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/log_path.py +0 -0
  75. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/modes.py +0 -0
  76. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/nests_runs.py +0 -0
  77. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/note_path.py +0 -0
  78. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/notebook_kernel.py +0 -0
  79. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/output_html.py +0 -0
  80. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/parse_run_view.py +0 -0
  81. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/print_rows.py +0 -0
  82. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/python_blocks.py +0 -0
  83. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/read_config.py +0 -0
  84. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/read_path.py +0 -0
  85. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/render_markdown.py +0 -0
  86. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/retries.py +0 -0
  87. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/run_command.py +0 -0
  88. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/run_control.py +0 -0
  89. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/run_output.py +0 -0
  90. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/run_status.py +0 -0
  91. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/__init__.py +0 -0
  92. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/describe_event.py +0 -0
  93. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/exit_signal.py +0 -0
  94. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/hold_folder.py +0 -0
  95. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/invocation_meta.py +0 -0
  96. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/new_param_run_id.py +0 -0
  97. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/new_run_id.py +0 -0
  98. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  99. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/param_args.py +0 -0
  100. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/param_grid.py +0 -0
  101. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/param_run_name.py +0 -0
  102. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/plan_runs.py +0 -0
  103. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/prepare_experiment.py +0 -0
  104. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/run_folder.py +0 -0
  105. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/run_headless.py +0 -0
  106. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/runs_parent.py +0 -0
  107. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/shell_command.py +0 -0
  108. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/signal_tree.py +0 -0
  109. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/stop_then_kill.py +0 -0
  110. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/wait_for_tree.py +0 -0
  111. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/runner/write_run_json.py +0 -0
  112. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/save_conflict_copy.py +0 -0
  113. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/save_note.py +0 -0
  114. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/saved_outputs.py +0 -0
  115. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/serve_app.py +0 -0
  116. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/server.py +0 -0
  117. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/shared_params.py +0 -0
  118. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/sort_href.py +0 -0
  119. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/sql_quote.py +0 -0
  120. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/elapsed.js +0 -0
  121. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/experiment.js +0 -0
  122. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/labnotes.css +0 -0
  123. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/local-time.js +0 -0
  124. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/new-note.js +0 -0
  125. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/note-edit.js +0 -0
  126. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/note-editor.js +0 -0
  127. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/row-link.js +0 -0
  128. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/run-cell.js +0 -0
  129. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/run-output.js +0 -0
  130. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/static/view-toggle.js +0 -0
  131. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/summarize_config.py +0 -0
  132. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/__init__.py +0 -0
  133. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/experiment_rows.py +0 -0
  134. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/experiment_run_ids.py +0 -0
  135. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/experiment_toml.py +0 -0
  136. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/experiments_ddl.py +0 -0
  137. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/file_prefix.py +0 -0
  138. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/file_text.py +0 -0
  139. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/iso_time.py +0 -0
  140. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/list_experiment_runs.py +0 -0
  141. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/note_text.py +0 -0
  142. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/path_glob.py +0 -0
  143. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/path_pattern.py +0 -0
  144. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/read_blocks.py +0 -0
  145. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/read_experiment.py +0 -0
  146. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/read_file.py +0 -0
  147. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/read_log.py +0 -0
  148. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tables/summarize_experiments.py +0 -0
  149. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/templates/base.html +0 -0
  150. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/templates/editor_imports.html +0 -0
  151. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/templates/experiment.html +0 -0
  152. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/templates/index.html +0 -0
  153. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/templates/note.html +0 -0
  154. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/templates/run.html +0 -0
  155. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/__init__.py +0 -0
  156. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/browse_app.py +0 -0
  157. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/browse_tui.py +0 -0
  158. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/config.py +0 -0
  159. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/dim.py +0 -0
  160. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/experiment_screen.py +0 -0
  161. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/experiments_screen.py +0 -0
  162. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/fill_table.py +0 -0
  163. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/format_elapsed.py +0 -0
  164. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/group_cells.py +0 -0
  165. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/invocation_columns.py +0 -0
  166. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/invocation_rows.py +0 -0
  167. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/leave_prompt.py +0 -0
  168. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/log_block.py +0 -0
  169. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/log_blocks.py +0 -0
  170. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/reload_on_change.py +0 -0
  171. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/run_app.py +0 -0
  172. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/run_cells.py +0 -0
  173. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/run_output.py +0 -0
  174. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/run_screen.py +0 -0
  175. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/run_tui.py +0 -0
  176. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/runs_table.py +0 -0
  177. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/tui/stopped_title.py +0 -0
  178. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/utils/__init__.py +0 -0
  179. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/utils/format_bytes.py +0 -0
  180. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/utils/run_usage.py +0 -0
  181. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/wait_for_note.py +0 -0
  182. {labnotes-0.0.110 → labnotes-0.0.112}/src/labnotes/watch_rows.py +0 -0
  183. {labnotes-0.0.110 → labnotes-0.0.112}/testing-conventions.toml +0 -0
  184. {labnotes-0.0.110 → labnotes-0.0.112}/tests/integration/conftest.py +0 -0
  185. {labnotes-0.0.110 → labnotes-0.0.112}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.110
3
+ Version: 0.0.112
4
4
  Summary: Browse experiment designs, notes, and run data from a folder.
5
5
  Project-URL: Repository, https://github.com/thekevinscott/labnotes
6
6
  Author-email: Kevin Scott <me@thekevinscott.com>
@@ -25,8 +25,8 @@ When `experiment.toml` sits next to `.labnotes.toml`, the root is the one experi
25
25
  - `files`: `experiment`, `filename`, `path`, `ctime`, `mtime`.
26
26
  - `runs`: `experiment`, `id`, `folder`, `created`, `ended`, `exit_code`,
27
27
  `error`, `stdout`, `stderr`, `params` as JSON, `meta` as JSON (null without
28
- a `meta` command), and `run`, the whole `run.json` as JSON, read from each run
29
- folder's `run.json`.
28
+ a `meta` command), `command`, and `run`, the whole `run.json` as JSON, read
29
+ from each run folder's `run.json`.
30
30
 
31
31
  Keys come from folder names, so renaming a folder renames its rows on the next
32
32
  read. Paths are relative to the root, and times are UTC ISO 8601.
@@ -75,9 +75,11 @@ inside it (`runs/20261020-000429-brave-fox/quiet-river`); the run id is that pat
75
75
  script so it writes its outputs there.
76
76
  Each param is appended as `--name value`, with the name as written (use
77
77
  `source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
78
- and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
79
- code, `ended_by`, `signal`), `stdout.log` and `stderr.log` to the
80
- run folder. `ended_by` is `"timeout"`, `"stop"` (labnotes stopped it),
78
+ and tables as JSON. labnotes writes `run.json` (params, `command`, created,
79
+ ended, exit code, `ended_by`, `signal`), `stdout.log` and `stderr.log` to the
80
+ run folder. `command` is the shell command the run executed, with `{run_dir}`
81
+ and the params filled in, written before the run starts, so a killed run has it
82
+ and a later edit to `experiment.toml` doesn't hide what a run did. `ended_by` is `"timeout"`, `"stop"` (labnotes stopped it),
81
83
  `"signal"` (a signal labnotes didn't send, like the out-of-memory killer),
82
84
  `"lost"` (its process died unseen; the daemon marks these when it starts, with
83
85
  `ended` set and `exit_code` null) or null (the script exited itself). `signal` is the signal number read from the
@@ -121,6 +123,13 @@ shows a waiting retry as `retry in 30s` and the end as `gave up (exit 75)`;
121
123
 
122
124
  Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs --format json`; a run with no `ended` is still going. Each run lists `size`, the bytes in its run folder, and a running run lists `memory`, the resident bytes of its command and everything it started.
123
125
 
126
+ While a run goes, labnotes appends a memory sample to `memory.jsonl` in its run
127
+ folder every 5 seconds: one JSON line, `{"time": <unix seconds>, "rss": <bytes>,
128
+ "processes": <count>}`, for the command and everything it started. It keeps
129
+ every sample, so a run killed for memory still shows the climb.
130
+ `memory_interval` in `experiment.toml` (or `[experiment]` in `.labnotes.toml`)
131
+ sets the seconds between samples; 0 turns it off.
132
+
124
133
  In a terminal, `run` shows a live table of runs (status, time, memory and
125
134
  folder size) and stays
126
135
  open when they finish. Failed runs show their status in red; `f` hides or shows them, and `r` on one
@@ -0,0 +1,10 @@
1
+ import math
2
+
3
+
4
+ def is_non_negative_number(value: object) -> bool:
5
+ return (
6
+ isinstance(value, int | float)
7
+ and not isinstance(value, bool)
8
+ and math.isfinite(value)
9
+ and value >= 0
10
+ )
@@ -0,0 +1,2 @@
1
+ # Seconds between memory samples, unless experiment.toml sets `memory_interval`.
2
+ DEFAULT_MEMORY_INTERVAL = 5.0
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
  from typing import Any, Protocol
5
5
 
6
6
  from labnotes.default_counts import DEFAULT_COUNTS
7
+ from labnotes.memory_interval import DEFAULT_MEMORY_INTERVAL
7
8
  from labnotes.modes import DEFAULT_MODE
8
9
  from labnotes.retries import DEFAULT_RETRIES
9
10
 
@@ -66,3 +67,4 @@ class ExperimentConfig:
66
67
  concurrency: int = 1
67
68
  retries: int = DEFAULT_RETRIES
68
69
  meta: str | None = None
70
+ memory_interval: float = DEFAULT_MEMORY_INTERVAL
@@ -9,6 +9,7 @@ import psutil
9
9
 
10
10
  from labnotes.log_path import LOG_ENV
11
11
  from labnotes.runner.exit_signal import exit_signal
12
+ from labnotes.runner.record_memory import record_memory
12
13
  from labnotes.runner.shell_command import shell_command
13
14
  from labnotes.runner.stop_then_kill import GRACE_SECONDS
14
15
  from labnotes.runner.wait_for_tree import wait_for_tree
@@ -30,9 +31,13 @@ async def execute_run(
30
31
  stop: Callable[[], None] = lambda: None,
31
32
  stopped: Callable[[], bool] = lambda: False,
32
33
  meta: dict[str, object] | None = None,
34
+ *,
35
+ memory_interval: float,
33
36
  ) -> dict[str, object]:
37
+ resolved = shell_command(command, folder, params)
34
38
  run: dict[str, object] = {
35
39
  "params": params,
40
+ "command": resolved,
36
41
  "created": iso_time(time.time()),
37
42
  "ended": None,
38
43
  "exit_code": None,
@@ -51,7 +56,7 @@ async def execute_run(
51
56
  ):
52
57
  try:
53
58
  process = await asyncio.create_subprocess_shell(
54
- shell_command(command, folder, params),
59
+ resolved,
55
60
  cwd=root,
56
61
  env={
57
62
  "FORCE_COLOR": "1",
@@ -67,9 +72,17 @@ async def execute_run(
67
72
  else:
68
73
  running.add(process)
69
74
  run["pid"] = process.pid
75
+ created = None
70
76
  with contextlib.suppress(psutil.Error):
71
- run["pid_created"] = psutil.Process(process.pid).create_time()
77
+ run["pid_created"] = created = psutil.Process(process.pid).create_time()
72
78
  write_run_json(folder, run)
79
+ sampler = None
80
+ if memory_interval > 0 and created is not None:
81
+ sampler = asyncio.create_task(
82
+ record_memory(
83
+ folder / "memory.jsonl", process.pid, created, memory_interval
84
+ )
85
+ )
73
86
  exited = asyncio.ensure_future(process.wait())
74
87
  kill_later = None
75
88
  try:
@@ -98,6 +111,9 @@ async def execute_run(
98
111
  finally:
99
112
  if kill_later is not None:
100
113
  kill_later.cancel()
114
+ if sampler is not None:
115
+ sampler.cancel()
116
+ await asyncio.wait({sampler})
101
117
  running.discard(process)
102
118
  run["ended"] = iso_time(time.time())
103
119
  write_run_json(folder, run)
@@ -7,6 +7,8 @@ from labnotes.compose_experiment import compose_experiment
7
7
  from labnotes.default_counts import DEFAULT_COUNTS
8
8
  from labnotes.models import ExperimentConfig
9
9
  from labnotes.is_non_negative_int import is_non_negative_int
10
+ from labnotes.is_non_negative_number import is_non_negative_number
11
+ from labnotes.memory_interval import DEFAULT_MEMORY_INTERVAL
10
12
  from labnotes.is_positive_int import is_positive_int
11
13
  from labnotes.modes import MODES
12
14
 
@@ -20,6 +22,7 @@ SETTINGS = {
20
22
  "concurrency",
21
23
  "retries",
22
24
  "meta",
25
+ "memory_interval",
23
26
  }
24
27
 
25
28
 
@@ -73,6 +76,18 @@ def read_experiment_config(
73
76
  meta = values.get("meta")
74
77
  if meta is not None and (not isinstance(meta, str) or not meta.strip()):
75
78
  raise ValueError(f"{path}: meta must be a string")
79
+ memory_interval = values.get("memory_interval", DEFAULT_MEMORY_INTERVAL)
80
+ if not is_non_negative_number(memory_interval):
81
+ raise ValueError(f"{path}: memory_interval must be 0 or more seconds")
76
82
  return ExperimentConfig(
77
- command, repeats, params, name, counts, mode, concurrency, retries, meta
83
+ command,
84
+ repeats,
85
+ params,
86
+ name,
87
+ counts,
88
+ mode,
89
+ concurrency,
90
+ retries,
91
+ meta,
92
+ memory_interval,
78
93
  )
@@ -0,0 +1,18 @@
1
+ import asyncio
2
+ import json
3
+ import time
4
+ from pathlib import Path
5
+
6
+ from labnotes.utils.tree_usage import tree_usage
7
+
8
+
9
+ async def record_memory(path: Path, pid: int, created: float, interval: float) -> None:
10
+ with path.open("a", encoding="utf-8") as samples:
11
+ while True:
12
+ usage = tree_usage(pid, created)
13
+ if usage is not None:
14
+ rss, processes = usage
15
+ sample = {"time": time.time(), "rss": rss, "processes": processes}
16
+ samples.write(json.dumps(sample) + "\n")
17
+ samples.flush()
18
+ await asyncio.sleep(interval)
@@ -261,6 +261,7 @@ async def run_experiment(
261
261
  stop=lambda: kill(run),
262
262
  stopped=lambda: run in stopped,
263
263
  meta=meta,
264
+ memory_interval=experiment.memory_interval,
264
265
  )
265
266
  del running[run], trees[run]
266
267
  report({"event": "ended", "run": run, **result})
@@ -10,6 +10,7 @@ from labnotes.tables.read_blocks import read_blocks
10
10
  from labnotes.tables.read_experiment import read_experiment
11
11
  from labnotes.tables.read_file import read_file
12
12
  from labnotes.tables.read_log import read_log
13
+ from labnotes.tables.read_memory import read_memory
13
14
  from labnotes.tables.read_run import read_run
14
15
 
15
16
  FILES = """
@@ -22,7 +23,7 @@ RUNS = """
22
23
  CREATE TABLE runs (
23
24
  experiment TEXT, id TEXT, folder TEXT, created TEXT, ended TEXT,
24
25
  exit_code INTEGER, error TEXT,
25
- stdout TEXT, stderr TEXT, params TEXT, meta TEXT, run TEXT,
26
+ stdout TEXT, stderr TEXT, params TEXT, meta TEXT, command TEXT, run TEXT,
26
27
  PRIMARY KEY (experiment, id)
27
28
  )
28
29
  """
@@ -39,6 +40,12 @@ CREATE TABLE blocks (
39
40
  PRIMARY KEY (experiment, run)
40
41
  )
41
42
  """
43
+ MEMORY = """
44
+ CREATE TABLE memory (
45
+ experiment TEXT, run TEXT, line INTEGER, time REAL, rss INTEGER, processes INTEGER,
46
+ PRIMARY KEY (experiment, run, line)
47
+ )
48
+ """
42
49
 
43
50
 
44
51
  def open_tables(config: LabnotesConfig) -> DirSQL:
@@ -78,6 +85,14 @@ def open_tables(config: LabnotesConfig) -> DirSQL:
78
85
  read_blocks, root, config.runs, experiment=config.experiment
79
86
  ),
80
87
  ),
88
+ Table(
89
+ name="memory",
90
+ ddl=MEMORY,
91
+ glob=path_glob(f"{config.runs}/memory.jsonl"),
92
+ on_file=partial(
93
+ read_memory, root, config.runs, experiment=config.experiment
94
+ ),
95
+ ),
81
96
  Table(
82
97
  name="experiment_configs",
83
98
  ddl=experiments_ddl(config),
@@ -0,0 +1,34 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from labnotes.tables.path_pattern import path_pattern
5
+
6
+
7
+ def read_memory(
8
+ root: Path, template: str, path: str, experiment: str | None = None
9
+ ) -> list[dict[str, object]]:
10
+ relative = Path(path).relative_to(root).as_posix()
11
+ found = path_pattern(f"{template}/memory.jsonl").fullmatch(relative)
12
+ if found is None:
13
+ return []
14
+ rows: list[dict[str, object]] = []
15
+ lines = Path(path).read_text(encoding="utf-8").splitlines()
16
+ for line, text in enumerate(lines):
17
+ # A run killed mid-write leaves a cut-off last line.
18
+ try:
19
+ sample = json.loads(text)
20
+ except json.JSONDecodeError:
21
+ continue
22
+ if not isinstance(sample, dict):
23
+ continue
24
+ rows.append(
25
+ {
26
+ "experiment": experiment or found["experiment"],
27
+ "run": found["run"],
28
+ "line": line,
29
+ "time": sample.get("time"),
30
+ "rss": sample.get("rss"),
31
+ "processes": sample.get("processes"),
32
+ }
33
+ )
34
+ return rows
@@ -26,6 +26,7 @@ def read_run(
26
26
  "stderr": f"{folder}/stderr.log",
27
27
  "params": json.dumps(run.get("params", {})),
28
28
  "meta": None if (meta := run.get("meta")) is None else json.dumps(meta),
29
+ "command": run.get("command"),
29
30
  "run": json.dumps(run),
30
31
  }
31
32
  ]
@@ -1,6 +1,6 @@
1
1
  import psutil
2
2
 
3
- from labnotes.utils.tree_rss import SAME_START_SECONDS
3
+ from labnotes.utils.tree_usage import SAME_START_SECONDS
4
4
 
5
5
 
6
6
  def process_alive(pid: int, created: float | None) -> bool:
@@ -0,0 +1,6 @@
1
+ from labnotes.utils.tree_usage import tree_usage
2
+
3
+
4
+ def tree_rss(pid: int, created: float) -> int | None:
5
+ usage = tree_usage(pid, created)
6
+ return None if usage is None else usage[0]
@@ -7,7 +7,7 @@ import psutil
7
7
  SAME_START_SECONDS = 0.01
8
8
 
9
9
 
10
- def tree_rss(pid: int, created: float) -> int | None:
10
+ def tree_usage(pid: int, created: float) -> tuple[int, int] | None:
11
11
  try:
12
12
  root = psutil.Process(pid)
13
13
  if abs(root.create_time() - created) > SAME_START_SECONDS:
@@ -24,4 +24,4 @@ def tree_rss(pid: int, created: float) -> int | None:
24
24
  for process in tree:
25
25
  with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied):
26
26
  sizes.append(process.memory_info().rss)
27
- return sum(sizes) if sizes else None
27
+ return (sum(sizes), len(sizes)) if sizes else None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes