Browse a long recording from disk instead of loading it into memory
What
- record/index.py: RecordingIndex streams forces.csv once and keeps only the
small things — one time, one validity flag, one frame id per row plus a byte
offset every 4096 rows (~6.5 MB per 500k rows). row_range() bisects the times,
read_rows() seeks to the nearest checkpoint and scans forward, and overview()
folds every row into its bucket during the same pass to build a per-axis
min/max envelope.
- read_rows(max_points, unit) decimates **while streaming**: each bucket keeps
the extreme row of every axis in the requested unit, so a long interval never
has to exist in memory in full and a shear spike survives a flat normal channel.
- RecordingReader: info() decimates above max_rows (default 200k) and marks
itself decimated, with total_rows and a duration taken from the frame index
rather than from the view; window()/window_all()/overview()/statistics() read
the interval being asked about. list/window semantics unchanged for small
recordings.
- force/source.py: StoreCurveSource (live memory) and RecordingCurveSource (disk
interval + cached envelope) implement one interface — extent, window,
envelope, statistics, duration_s — so ForcePlotsPanel asks a *source* instead
of knowing where the data lives.
- force_plots: reads through the source (window, envelope, statistics, extent),
which also replaces its last direct store accesses.
- playback_dialog: draws the whole-session view through window_all() in the unit
being shown (bounded by CHART_MAX_POINTS), and says so when the view is
decimated ("显示 N / 共 M 行") instead of letting the operator count points.
- experiment._run: a failure that lands while the files are opening can no longer
be overwritten by the writer's own transition to "recording" (found by a flaky
test; the state check now covers STATE_FAILED).
Why
The store releases old blocks above ~4.6 h and the replay loaded every CSV row
into Python lists before drawing anything, so neither the live browser nor the
replay could show an earlier interval of a long recording. The index makes the
disk the data source and memory a cache of what is on screen.
Verification
- shared/tests/test_history_source.py (new, 8 cases) over a 12 000-row recording:
the index reports its rows and reads a bounded interval; a large recording is
marked decimated with its true duration and total rows; a small one is not; an
interval read sees the exact rows including a single peak; a decimated window
keeps the peak; the envelope fits the whole session in ≤200 points, keeps the
spike and is cached; the disk and memory sources agree on extent, values and
statistics; the whole session can be read without holding it all. Shared
suites: 146 tests, 0 failures (three consecutive runs).
- apps/studio/tools/ui_check.py: the replay section now checks the index row
count, that an interval read spans only the interval, the envelope size and
total_rows. 324 checks, 0 failures.
- apps/studio/tests/test_ui_smoke.py: 43 tests, 36 pass; the 7 failures are the
unchanged sandbox/harness artifacts.
- D:/orisys_ws/_review_phase12/reproduce.py: all ten lines unchanged;
compileall, locale and prefs parity clean.
- apps/studio/docs/record-channel.md §8 documents the index, the reader API and
the unified source, and lists what is still hardware work.
Not in this change
- Maximum RSS / backlog / loss measurements at real resolution on the target ARM
board (needs the hardware), and range CSV export.
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
@@ -115,6 +115,8 @@
|
||||
"box_zoom_tip": "Switch to rubber-band zoom (drag a region on the chart)",
|
||||
"selection": "Selection {duration}s / {samples} samples — {detail}",
|
||||
"selection_empty": "No valid samples in the selection",
|
||||
"value_unavailable": "n/a",
|
||||
"value_unavailable_tip": "No curve is loaded for {channel}, so it has no value in this unit (raw counts are never shown in its place)",
|
||||
"browse_only_tip": "Drag or scroll on the chart to browse history"
|
||||
},
|
||||
"layout": {
|
||||
@@ -154,14 +156,21 @@
|
||||
"depth_state_approximate": "No reset or pre-roll history for this segment: the algorithm state is an approximation",
|
||||
"depth_warnings": "Recomputation notes: {detail}",
|
||||
"warnings": "Replay notes: {detail}",
|
||||
"decimated": "This recording is long, so the curves are a decimated view ({shown} of {total} rows; intervals are read from disk on demand)",
|
||||
"warn_config_changed": "the config differs from recording time, so the recomputed depth may differ",
|
||||
"warn_sdk_changed": "the SDK version differs from recording time, so the recomputation may differ",
|
||||
"warn_config_missing": "no config hash was recorded, so recomputation cannot be verified",
|
||||
"warn_depth_recomputed": "depth comes from offline recomputation, not from stored data",
|
||||
"warn_rows_skipped": "some unparsable CSV rows were skipped",
|
||||
"warn_frames_dropped": "frames were lost to a full queue; only their index rows remain (shown as \"not recorded\")",
|
||||
"warn_out_of_order": "this recording was written by an older version: its rows are not in time order, so the replay re-sorts them and window edges may differ",
|
||||
"warn_state_across_segment": "the algorithm state cannot be carried across a segment",
|
||||
"warn_state_approximate": "the algorithm state is an approximation"
|
||||
"warn_state_approximate": "the algorithm state is an approximation",
|
||||
"warn_state_approximate_after_rewind": "the video was reopened to reach an earlier frame, so the state may differ",
|
||||
"warn_state_too_far_back": "the zeroing is too far back: reading there would exceed the replay budget, so the state is approximate",
|
||||
"warn_reset_frame_not_indexed": "the zeroing frame is not in the frame index, so the state cannot be rebuilt from it",
|
||||
"warn_reset_frame_not_in_video": "the zeroing frame never reached the video, so the state cannot be rebuilt from it",
|
||||
"warn_reset_not_applied": "the replay could not re-apply the zeroing, so the state may differ"
|
||||
},
|
||||
"record": {
|
||||
"label": "Record",
|
||||
@@ -177,6 +186,8 @@
|
||||
"start_failed": "Could not start recording: {error}",
|
||||
"finished": "Recording finished — {frames} frames / {segments} segment(s) in {path}",
|
||||
"finished_with_problems": "Recording finished with problems: {detail}",
|
||||
"closing": "Closing the recording files…",
|
||||
"failed_state": "The recording failed (see the summary and the log)",
|
||||
"alarm": "Recording problem: {detail}"
|
||||
},
|
||||
"cal": {
|
||||
@@ -204,8 +215,12 @@
|
||||
"open_record_tip": "Reopen a saved calibration session to inspect every point's samples and diagnostics",
|
||||
"open_record_filter": "Calibration records (*.json)",
|
||||
"open_record_ok": "Loaded {path} ({points} point(s))",
|
||||
"open_record_device": "Recorded on: {device}",
|
||||
"open_record_warnings": "Note: {detail}",
|
||||
"open_record_failed": "Could not open the calibration record: {error}",
|
||||
"apply_mismatch_title": "Calibration does not match the target device",
|
||||
"apply_mismatch": "This coefficient file belongs to: {device}\nMismatch: {detail}\n\nCurrent target: {sensor_type} / {config}\n\nApplying it would load coefficients that were not measured on this device.",
|
||||
"apply_mismatch_skipped": "Apply cancelled (the calibration belongs to another device)",
|
||||
"sn": "SN",
|
||||
"sn_placeholder": "Serial number (optional)",
|
||||
"live_raw_label": "Live raw",
|
||||
@@ -255,11 +270,13 @@
|
||||
"export_local": "Also wrote {path}",
|
||||
"export_warnings": "Skipped: {detail}",
|
||||
"export_failed": "Export failed: {error}",
|
||||
"export_no_model": "Nothing to export: {detail}",
|
||||
"apply_title": "Apply calibration",
|
||||
"apply_confirm": "Apply the calibration just exported to the current device?\n\nSensor type: {sensor_type}\nConfig: {config}\n\nChoosing No keeps the exported file and leaves the running device untouched.",
|
||||
"apply_skipped": "Exported; not applied to the device",
|
||||
"apply_pending": "Requested calibration for {sensor_type}; waiting for the device to confirm…",
|
||||
"applied": "Device confirmed the calibration is in effect (version {version})",
|
||||
"applied_none": "Device confirmed: no calibration is loaded (the column stays empty)",
|
||||
"export_only": "Exported coefficients to {path} (not applied to the device)",
|
||||
"apply_no_target": "No device to apply to (run an acquisition first)"
|
||||
},
|
||||
@@ -286,6 +303,7 @@
|
||||
"video_ended": "Video ended",
|
||||
"camera_disconnected": "Camera disconnected",
|
||||
"reset_ok": "Tracker reset",
|
||||
"reset_pending": "Zeroing requested; waiting for the acquisition thread to confirm…",
|
||||
"finger_run_unavailable": "Run is not available for this sensor type",
|
||||
"video_required": "Select a video file first",
|
||||
"config_missing": "Sensor config not found for {type}",
|
||||
|
||||
@@ -115,6 +115,8 @@
|
||||
"box_zoom_tip": "切换为框选放大(拖动图表框选区域)",
|
||||
"selection": "选区 {duration}s / {samples} 点 — {detail}",
|
||||
"selection_empty": "选区无有效样本",
|
||||
"value_unavailable": "不可用",
|
||||
"value_unavailable_tip": "{channel} 轴未加载标定曲线,因此没有该单位的数值(不会用原始值冒充)",
|
||||
"browse_only_tip": "拖动或滚轮即可进入历史浏览"
|
||||
},
|
||||
"layout": {
|
||||
@@ -154,14 +156,21 @@
|
||||
"depth_state_approximate": "本段缺少归零或预卷历史,算法参考状态为近似重建",
|
||||
"depth_warnings": "重算提示: {detail}",
|
||||
"warnings": "回放提示: {detail}",
|
||||
"decimated": "该录制较长,曲线为降采样视图(显示 {shown} / 共 {total} 行;区间数据按需从磁盘读取)",
|
||||
"warn_config_changed": "配置文件与录制时不一致,重算出的深度可能与当时不同",
|
||||
"warn_sdk_changed": "SDK 版本与录制时不一致,重算结果可能不同",
|
||||
"warn_config_missing": "找不到配置哈希,无法校验重算一致性",
|
||||
"warn_depth_recomputed": "深度由离线重算得到,不是当时保存的数据",
|
||||
"warn_rows_skipped": "有无法解析的 CSV 行被跳过",
|
||||
"warn_frames_dropped": "有帧在队列溢出时丢失,仅保留索引记录(回放显示为“该帧未录制”)",
|
||||
"warn_out_of_order": "该录制由旧版本写入,行序不是时间序;回放已按时间重排,区间边界可能与当时不同",
|
||||
"warn_state_across_segment": "重算不能跨分段延续算法状态",
|
||||
"warn_state_approximate": "算法参考状态为近似重建"
|
||||
"warn_state_approximate": "算法参考状态为近似重建",
|
||||
"warn_state_approximate_after_rewind": "为回到更早的帧重新打开了视频,参考状态可能偏离当时",
|
||||
"warn_state_too_far_back": "归零点过于久远,需要读取的帧数超出上限,已按近似状态重建",
|
||||
"warn_reset_frame_not_indexed": "归零帧不在帧索引中,无法从该点重建状态",
|
||||
"warn_reset_frame_not_in_video": "归零帧未写入视频,无法从该点重建状态",
|
||||
"warn_reset_not_applied": "重放无法重新执行归零,参考状态可能偏离当时"
|
||||
},
|
||||
"record": {
|
||||
"label": "记录",
|
||||
@@ -177,6 +186,8 @@
|
||||
"start_failed": "无法开始记录: {error}",
|
||||
"finished": "记录完成 — {frames} 帧 / {segments} 段,已写入 {path}",
|
||||
"finished_with_problems": "记录完成,但存在问题: {detail}",
|
||||
"closing": "正在关闭记录文件…",
|
||||
"failed_state": "记录已失败(详见摘要与日志)",
|
||||
"alarm": "记录异常: {detail}"
|
||||
},
|
||||
"cal": {
|
||||
@@ -204,8 +215,12 @@
|
||||
"open_record_tip": "重新打开已保存的标定会话,检查每个点的完整采样与诊断状态",
|
||||
"open_record_filter": "标定记录 (*.json)",
|
||||
"open_record_ok": "已载入 {path}({points} 个点)",
|
||||
"open_record_device": "记录设备: {device}",
|
||||
"open_record_warnings": "注意: {detail}",
|
||||
"open_record_failed": "打开标定记录失败: {error}",
|
||||
"apply_mismatch_title": "标定与目标设备不匹配",
|
||||
"apply_mismatch": "该标定文件属于: {device}\n不匹配项: {detail}\n\n当前目标: {sensor_type} / {config}\n\n继续应用会把不适用于本设备的系数写入运行中的设备。",
|
||||
"apply_mismatch_skipped": "已取消应用(标定身份与目标设备不匹配)",
|
||||
"sn": "序列号",
|
||||
"sn_placeholder": "序列号(可选)",
|
||||
"live_raw_label": "实时原始值",
|
||||
@@ -255,11 +270,13 @@
|
||||
"export_local": "同时写入 {path}",
|
||||
"export_warnings": "跳过: {detail}",
|
||||
"export_failed": "导出失败: {error}",
|
||||
"export_no_model": "没有可导出的模型: {detail}",
|
||||
"apply_title": "应用标定",
|
||||
"apply_confirm": "将刚导出的标定应用到当前设备?\n\n设备类型: {sensor_type}\n配置: {config}\n\n选择「否」只保留导出文件,不改动运行中的设备。",
|
||||
"apply_skipped": "已导出,未应用到设备",
|
||||
"apply_pending": "已请求把标定应用到 {sensor_type},等待设备确认…",
|
||||
"applied": "设备已确认标定生效(版本 {version})",
|
||||
"applied_none": "设备已确认:当前没有加载标定(该项留空)",
|
||||
"export_only": "已导出系数 {path}(未应用到设备)",
|
||||
"apply_no_target": "没有可应用的设备(请先运行一次采集)"
|
||||
},
|
||||
@@ -286,6 +303,7 @@
|
||||
"video_ended": "视频已结束",
|
||||
"camera_disconnected": "摄像机已断开",
|
||||
"reset_ok": "追踪器已重置",
|
||||
"reset_pending": "已请求归零,等待采集线程确认…",
|
||||
"finger_run_unavailable": "此传感器类型暂不支持运行",
|
||||
"video_required": "请先选择视频文件",
|
||||
"config_missing": "未找到传感器配置: {type}",
|
||||
|
||||
Reference in New Issue
Block a user