@meri-imperiumi/signalk-dead-reckoning 0.13.2 → 0.13.3
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.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/plugin/db.js +26 -1
- package/tests/db.test.js +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.3] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **The state flush no longer writes rows that did not change, and WAL checkpoint bursts are bounded** (`plugin/db.js`; found by file-activity profiling of a boat server, which showed the DR store's WAL parked at the 4 MB auto-checkpoint high-water and ~40 MB/day of flash writes at the 60 s flush cadence): `setState()` now reads the stored value and skips the write when it is identical — while anchored most flush scalars (`dr_log_nm`, `dr_trip_log_nm`, `dr_trip_start_ms`, `dr_log_since_origin`, `dr_underway_since_origin_s`, `dr_origin_error_nm`) are unchanged, so each flush previously dirtied a 4 kB WAL page per row for nothing; suppressing no-op upserts cuts the anchored flush to the rows that genuinely move (`last_known_good_fix`, whose fix timestamp advances, and `dr_elapsed_since_origin_s`, the wall-clock cone input). `updated_at` then stays at the value's last real change, which is the truthful reading of the column; nothing in the codebase reads it. The database also opens with `PRAGMA wal_autocheckpoint = 256` and `PRAGMA journal_size_limit = 1048576`: the default 1000-page (4 MB) threshold turned every WAL generation into a single ~4 MB copy-back burst on SD-card hardware, and the WAL file parked at its high-water size forever — the same bytes now flow as four 1 MB bursts and the file shrinks back between checkpoints.
|
|
15
|
+
|
|
10
16
|
## [0.13.2] - 2026-10-11
|
|
11
17
|
|
|
12
18
|
### Fixed
|
package/package.json
CHANGED
package/plugin/db.js
CHANGED
|
@@ -206,6 +206,15 @@ function openDatabase(dbPath) {
|
|
|
206
206
|
// sustained minute after minute on SD-card boat hardware — for that
|
|
207
207
|
// small window is the right default. FULL would fsync every commit.
|
|
208
208
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
209
|
+
// Bound the WAL's write bursts: the default auto-checkpoint threshold
|
|
210
|
+
// is 1000 pages (4 MB), so every WAL generation ends in a single ~4 MB
|
|
211
|
+
// copy back into the main database — a periodic flash-write spike on
|
|
212
|
+
// SD-card boat hardware. 256 pages (1 MB) spreads the same bytes over
|
|
213
|
+
// four smaller bursts, and journal_size_limit lets the WAL file shrink
|
|
214
|
+
// back to 1 MB after each checkpoint instead of parking forever at its
|
|
215
|
+
// high-water size (observed at ~4 MB on production stores).
|
|
216
|
+
db.exec("PRAGMA wal_autocheckpoint = 256");
|
|
217
|
+
db.exec("PRAGMA journal_size_limit = 1048576");
|
|
209
218
|
db.exec("PRAGMA foreign_keys = ON");
|
|
210
219
|
for (const stmt of SCHEMA_DDL) {
|
|
211
220
|
db.exec(stmt);
|
|
@@ -351,17 +360,33 @@ function getState(db, key) {
|
|
|
351
360
|
/**
|
|
352
361
|
* Writes a scalar value to `dr_state_store`, upserting.
|
|
353
362
|
*
|
|
363
|
+
* Identical values are not written: the periodic state flush rewrites
|
|
364
|
+
* the same scalars every cycle (log totals, origin error, trip state),
|
|
365
|
+
* and on SD-card boat hardware even a no-op upsert dirties a 4 kB page
|
|
366
|
+
* in the WAL, so suppressing unchanged writes is the single biggest
|
|
367
|
+
* flash-wear lever this store has. `updated_at` then stays at the
|
|
368
|
+
* value's last real change, which is the truthful reading of the
|
|
369
|
+
* column anyway. Column reads elsewhere only consume `value`.
|
|
370
|
+
*
|
|
354
371
|
* @param {import("node:sqlite").DatabaseSync} db
|
|
355
372
|
* @param {string} key
|
|
356
373
|
* @param {string} value
|
|
357
|
-
* @returns {
|
|
374
|
+
* @returns {boolean} True when a write happened, false when the stored
|
|
375
|
+
* value was already identical.
|
|
358
376
|
*/
|
|
359
377
|
function setState(db, key, value) {
|
|
378
|
+
const existing = db
|
|
379
|
+
.prepare("SELECT value FROM dr_state_store WHERE key = ?")
|
|
380
|
+
.get(key);
|
|
381
|
+
if (existing && existing.value === value) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
360
384
|
db.prepare(
|
|
361
385
|
`INSERT INTO dr_state_store (key, value, updated_at)
|
|
362
386
|
VALUES (?, ?, ?)
|
|
363
387
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
|
364
388
|
).run(key, value, new Date().toISOString());
|
|
389
|
+
return true;
|
|
365
390
|
}
|
|
366
391
|
|
|
367
392
|
/**
|
package/tests/db.test.js
CHANGED
|
@@ -243,6 +243,39 @@ test("setState upserts and getState reads back", () => {
|
|
|
243
243
|
db.close();
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
test("setState skips identical values (flash-wear guard)", () => {
|
|
247
|
+
const db = openDatabase(join(tempDir, "c.sqlite"));
|
|
248
|
+
assert.strictEqual(setState(db, "dr_log_nm", "123.45"), true);
|
|
249
|
+
const firstUpdatedAt = db
|
|
250
|
+
.prepare("SELECT updated_at FROM dr_state_store WHERE key = ?")
|
|
251
|
+
.get("dr_log_nm").updated_at;
|
|
252
|
+
// Identical write is suppressed: no updated_at churn, no WAL page dirtied
|
|
253
|
+
assert.strictEqual(setState(db, "dr_log_nm", "123.45"), false);
|
|
254
|
+
assert.strictEqual(
|
|
255
|
+
db
|
|
256
|
+
.prepare("SELECT updated_at FROM dr_state_store WHERE key = ?")
|
|
257
|
+
.get("dr_log_nm").updated_at,
|
|
258
|
+
firstUpdatedAt,
|
|
259
|
+
);
|
|
260
|
+
// A changed value still writes
|
|
261
|
+
assert.strictEqual(setState(db, "dr_log_nm", "124.00"), true);
|
|
262
|
+
assert.strictEqual(getState(db, "dr_log_nm"), "124.00");
|
|
263
|
+
db.close();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("openDatabase bounds WAL checkpoint bursts and file high-water", () => {
|
|
267
|
+
const db = openDatabase(join(tempDir, "c.sqlite"));
|
|
268
|
+
assert.strictEqual(
|
|
269
|
+
db.prepare("PRAGMA wal_autocheckpoint").get()["wal_autocheckpoint"],
|
|
270
|
+
256,
|
|
271
|
+
);
|
|
272
|
+
assert.strictEqual(
|
|
273
|
+
db.prepare("PRAGMA journal_size_limit").get()["journal_size_limit"],
|
|
274
|
+
1048576,
|
|
275
|
+
);
|
|
276
|
+
db.close();
|
|
277
|
+
});
|
|
278
|
+
|
|
246
279
|
test("dr_matrix_bins primary key enforces bin-level uniqueness", () => {
|
|
247
280
|
const db = openDatabase(join(tempDir, "d.sqlite"));
|
|
248
281
|
const ins = db.prepare(
|