@camstack/shm-ring 1.0.16 → 1.0.18
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.
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { DecodedFrame, FrameHandle, IScopedLogger } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* Idle grace before a cached reader whose segment stopped appearing in handles
|
|
4
|
+
* is `munmap`'d and evicted. Longer than any legitimate quiet gap between
|
|
5
|
+
* frames on a live segment (a stalled camera, a sparse occupancy-recheck poll,
|
|
6
|
+
* a keyframe wait) so a still-live segment is not churned; short enough that a
|
|
7
|
+
* rotated-out / restarted-decoder segment's mapping is reclaimed promptly
|
|
8
|
+
* instead of leaking until subscription teardown. Eviction is functionally
|
|
9
|
+
* safe (a re-appearing shmId re-opens the segment), so this only trades a rare
|
|
10
|
+
* remap syscall against how fast `/dev/shm` is reclaimed under rotation churn.
|
|
11
|
+
*/
|
|
12
|
+
export declare const STALE_READER_TTL_MS = 15000;
|
|
13
|
+
/** Injectable knobs for the cache — all optional, all defaulted. */
|
|
14
|
+
export interface FrameRingReaderCacheOptions {
|
|
15
|
+
/** Wall clock, injectable for deterministic tests. Defaults to `Date.now`. */
|
|
16
|
+
readonly now?: () => number;
|
|
17
|
+
/** Override the idle eviction grace. Defaults to {@link STALE_READER_TTL_MS}. */
|
|
18
|
+
readonly staleReaderTtlMs?: number;
|
|
19
|
+
}
|
|
2
20
|
/**
|
|
3
21
|
* Opens (and caches) a `FrameRingReader` per `shmId` and reads the pixels a
|
|
4
22
|
* `FrameHandle` refers to. Single-consumer — one cache per frame subscription.
|
|
@@ -6,8 +24,13 @@ import { DecodedFrame, FrameHandle, IScopedLogger } from '@camstack/types';
|
|
|
6
24
|
export declare class FrameRingReaderCache {
|
|
7
25
|
private readonly rings;
|
|
8
26
|
private readonly logger;
|
|
27
|
+
private readonly now;
|
|
28
|
+
private readonly staleReaderTtlMs;
|
|
9
29
|
private closed;
|
|
10
|
-
constructor(logger?: IScopedLogger);
|
|
30
|
+
constructor(logger?: IScopedLogger, options?: FrameRingReaderCacheOptions);
|
|
31
|
+
/** Number of shm segments currently mapped by this cache. Diagnostic — also
|
|
32
|
+
* lets a test observe stale-reader eviction. */
|
|
33
|
+
get mappedSegmentCount(): number;
|
|
11
34
|
/**
|
|
12
35
|
* Read the pixels a `FrameHandle` refers to and return them as a
|
|
13
36
|
* `DecodedFrame`. Returns `null` when the ring slot was recycled before the
|
|
@@ -18,4 +41,21 @@ export declare class FrameRingReaderCache {
|
|
|
18
41
|
close(): void;
|
|
19
42
|
/** Get the cached reader for a handle's segment, opening it on first use. */
|
|
20
43
|
private ringFor;
|
|
44
|
+
/**
|
|
45
|
+
* Close (`munmap`) and evict every cached reader — except `keepShmId`, the
|
|
46
|
+
* one being looked up right now — whose segment stopped appearing in handles
|
|
47
|
+
* for longer than `staleReaderTtlMs`. Cheap: a no-op unless more than one
|
|
48
|
+
* segment is mapped (steady state holds exactly one per subscription), so the
|
|
49
|
+
* common per-read path does not even iterate.
|
|
50
|
+
*/
|
|
51
|
+
private sweepStaleRings;
|
|
52
|
+
/**
|
|
53
|
+
* Unmap one cached segment and drop it from the map. The native `close()` is
|
|
54
|
+
* itself idempotent (see `native.ts`), and deleting from the map before the
|
|
55
|
+
* `munmap` keeps eviction idempotent even if `close()` throws — the reader is
|
|
56
|
+
* never revived. This only `munmap`s the reader's own view; it does NOT
|
|
57
|
+
* `shm_unlink` (the decoder that created the segment owns the name), which is
|
|
58
|
+
* exactly what lets the OS reclaim the backing memory once every view closes.
|
|
59
|
+
*/
|
|
60
|
+
private closeRing;
|
|
21
61
|
}
|
package/dist/index.js
CHANGED
|
@@ -570,8 +570,6 @@ var FrameRingReader = class extends FrameRingBase {
|
|
|
570
570
|
};
|
|
571
571
|
}
|
|
572
572
|
};
|
|
573
|
-
//#endregion
|
|
574
|
-
//#region src/frame-ring-reader-cache.ts
|
|
575
573
|
/**
|
|
576
574
|
* Opens (and caches) a `FrameRingReader` per `shmId` and reads the pixels a
|
|
577
575
|
* `FrameHandle` refers to. Single-consumer — one cache per frame subscription.
|
|
@@ -579,9 +577,18 @@ var FrameRingReader = class extends FrameRingBase {
|
|
|
579
577
|
var FrameRingReaderCache = class {
|
|
580
578
|
rings = /* @__PURE__ */ new Map();
|
|
581
579
|
logger;
|
|
580
|
+
now;
|
|
581
|
+
staleReaderTtlMs;
|
|
582
582
|
closed = false;
|
|
583
|
-
constructor(logger) {
|
|
583
|
+
constructor(logger, options) {
|
|
584
584
|
this.logger = logger;
|
|
585
|
+
this.now = options?.now ?? Date.now;
|
|
586
|
+
this.staleReaderTtlMs = options?.staleReaderTtlMs ?? 15e3;
|
|
587
|
+
}
|
|
588
|
+
/** Number of shm segments currently mapped by this cache. Diagnostic — also
|
|
589
|
+
* lets a test observe stale-reader eviction. */
|
|
590
|
+
get mappedSegmentCount() {
|
|
591
|
+
return this.rings.size;
|
|
585
592
|
}
|
|
586
593
|
/**
|
|
587
594
|
* Read the pixels a `FrameHandle` refers to and return them as a
|
|
@@ -616,28 +623,28 @@ var FrameRingReaderCache = class {
|
|
|
616
623
|
close() {
|
|
617
624
|
if (this.closed) return;
|
|
618
625
|
this.closed = true;
|
|
619
|
-
for (const [shmId, ring] of this.rings)
|
|
620
|
-
ring.segment.close();
|
|
621
|
-
} catch (err) {
|
|
622
|
-
this.logger?.warn("frame-ring reader: segment close failed", { meta: {
|
|
623
|
-
shmId,
|
|
624
|
-
error: (0, _camstack_types.errMsg)(err)
|
|
625
|
-
} });
|
|
626
|
-
}
|
|
626
|
+
for (const [shmId, ring] of this.rings) this.closeRing(shmId, ring);
|
|
627
627
|
this.rings.clear();
|
|
628
628
|
}
|
|
629
629
|
/** Get the cached reader for a handle's segment, opening it on first use. */
|
|
630
630
|
ringFor(handle) {
|
|
631
|
+
const now = this.now();
|
|
631
632
|
const cached = this.rings.get(handle.shmId);
|
|
632
|
-
if (cached)
|
|
633
|
+
if (cached) {
|
|
634
|
+
cached.lastUsedAt = now;
|
|
635
|
+
this.sweepStaleRings(now, handle.shmId);
|
|
636
|
+
return cached;
|
|
637
|
+
}
|
|
633
638
|
const slotByteLength = computeSlotByteLength(handle.width, handle.height, handle.format);
|
|
634
639
|
try {
|
|
635
640
|
const segment = openSegment(handle.shmId, computeSegmentSize(handle.slotCount, slotByteLength));
|
|
636
641
|
const ring = {
|
|
637
642
|
segment,
|
|
638
|
-
reader: new FrameRingReader(segment.buffer, handle.shmId, handle.slotCount, slotByteLength, handle.nodeId)
|
|
643
|
+
reader: new FrameRingReader(segment.buffer, handle.shmId, handle.slotCount, slotByteLength, handle.nodeId),
|
|
644
|
+
lastUsedAt: now
|
|
639
645
|
};
|
|
640
646
|
this.rings.set(handle.shmId, ring);
|
|
647
|
+
this.sweepStaleRings(now, handle.shmId);
|
|
641
648
|
return ring;
|
|
642
649
|
} catch (err) {
|
|
643
650
|
this.logger?.warn("frame-ring reader: openSegment failed", { meta: {
|
|
@@ -647,6 +654,44 @@ var FrameRingReaderCache = class {
|
|
|
647
654
|
return null;
|
|
648
655
|
}
|
|
649
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Close (`munmap`) and evict every cached reader — except `keepShmId`, the
|
|
659
|
+
* one being looked up right now — whose segment stopped appearing in handles
|
|
660
|
+
* for longer than `staleReaderTtlMs`. Cheap: a no-op unless more than one
|
|
661
|
+
* segment is mapped (steady state holds exactly one per subscription), so the
|
|
662
|
+
* common per-read path does not even iterate.
|
|
663
|
+
*/
|
|
664
|
+
sweepStaleRings(now, keepShmId) {
|
|
665
|
+
if (this.rings.size <= 1) return;
|
|
666
|
+
for (const [shmId, ring] of this.rings) {
|
|
667
|
+
if (shmId === keepShmId) continue;
|
|
668
|
+
if (now - ring.lastUsedAt <= this.staleReaderTtlMs) continue;
|
|
669
|
+
this.logger?.debug("frame-ring reader: evicting stale mapping", { meta: {
|
|
670
|
+
shmId,
|
|
671
|
+
idleMs: now - ring.lastUsedAt
|
|
672
|
+
} });
|
|
673
|
+
this.closeRing(shmId, ring);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Unmap one cached segment and drop it from the map. The native `close()` is
|
|
678
|
+
* itself idempotent (see `native.ts`), and deleting from the map before the
|
|
679
|
+
* `munmap` keeps eviction idempotent even if `close()` throws — the reader is
|
|
680
|
+
* never revived. This only `munmap`s the reader's own view; it does NOT
|
|
681
|
+
* `shm_unlink` (the decoder that created the segment owns the name), which is
|
|
682
|
+
* exactly what lets the OS reclaim the backing memory once every view closes.
|
|
683
|
+
*/
|
|
684
|
+
closeRing(shmId, ring) {
|
|
685
|
+
this.rings.delete(shmId);
|
|
686
|
+
try {
|
|
687
|
+
ring.segment.close();
|
|
688
|
+
} catch (err) {
|
|
689
|
+
this.logger?.warn("frame-ring reader: segment close failed", { meta: {
|
|
690
|
+
shmId,
|
|
691
|
+
error: (0, _camstack_types.errMsg)(err)
|
|
692
|
+
} });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
650
695
|
};
|
|
651
696
|
//#endregion
|
|
652
697
|
exports.FrameRingReader = FrameRingReader;
|
package/dist/index.mjs
CHANGED
|
@@ -569,8 +569,6 @@ var FrameRingReader = class extends FrameRingBase {
|
|
|
569
569
|
};
|
|
570
570
|
}
|
|
571
571
|
};
|
|
572
|
-
//#endregion
|
|
573
|
-
//#region src/frame-ring-reader-cache.ts
|
|
574
572
|
/**
|
|
575
573
|
* Opens (and caches) a `FrameRingReader` per `shmId` and reads the pixels a
|
|
576
574
|
* `FrameHandle` refers to. Single-consumer — one cache per frame subscription.
|
|
@@ -578,9 +576,18 @@ var FrameRingReader = class extends FrameRingBase {
|
|
|
578
576
|
var FrameRingReaderCache = class {
|
|
579
577
|
rings = /* @__PURE__ */ new Map();
|
|
580
578
|
logger;
|
|
579
|
+
now;
|
|
580
|
+
staleReaderTtlMs;
|
|
581
581
|
closed = false;
|
|
582
|
-
constructor(logger) {
|
|
582
|
+
constructor(logger, options) {
|
|
583
583
|
this.logger = logger;
|
|
584
|
+
this.now = options?.now ?? Date.now;
|
|
585
|
+
this.staleReaderTtlMs = options?.staleReaderTtlMs ?? 15e3;
|
|
586
|
+
}
|
|
587
|
+
/** Number of shm segments currently mapped by this cache. Diagnostic — also
|
|
588
|
+
* lets a test observe stale-reader eviction. */
|
|
589
|
+
get mappedSegmentCount() {
|
|
590
|
+
return this.rings.size;
|
|
584
591
|
}
|
|
585
592
|
/**
|
|
586
593
|
* Read the pixels a `FrameHandle` refers to and return them as a
|
|
@@ -615,28 +622,28 @@ var FrameRingReaderCache = class {
|
|
|
615
622
|
close() {
|
|
616
623
|
if (this.closed) return;
|
|
617
624
|
this.closed = true;
|
|
618
|
-
for (const [shmId, ring] of this.rings)
|
|
619
|
-
ring.segment.close();
|
|
620
|
-
} catch (err) {
|
|
621
|
-
this.logger?.warn("frame-ring reader: segment close failed", { meta: {
|
|
622
|
-
shmId,
|
|
623
|
-
error: errMsg(err)
|
|
624
|
-
} });
|
|
625
|
-
}
|
|
625
|
+
for (const [shmId, ring] of this.rings) this.closeRing(shmId, ring);
|
|
626
626
|
this.rings.clear();
|
|
627
627
|
}
|
|
628
628
|
/** Get the cached reader for a handle's segment, opening it on first use. */
|
|
629
629
|
ringFor(handle) {
|
|
630
|
+
const now = this.now();
|
|
630
631
|
const cached = this.rings.get(handle.shmId);
|
|
631
|
-
if (cached)
|
|
632
|
+
if (cached) {
|
|
633
|
+
cached.lastUsedAt = now;
|
|
634
|
+
this.sweepStaleRings(now, handle.shmId);
|
|
635
|
+
return cached;
|
|
636
|
+
}
|
|
632
637
|
const slotByteLength = computeSlotByteLength(handle.width, handle.height, handle.format);
|
|
633
638
|
try {
|
|
634
639
|
const segment = openSegment(handle.shmId, computeSegmentSize(handle.slotCount, slotByteLength));
|
|
635
640
|
const ring = {
|
|
636
641
|
segment,
|
|
637
|
-
reader: new FrameRingReader(segment.buffer, handle.shmId, handle.slotCount, slotByteLength, handle.nodeId)
|
|
642
|
+
reader: new FrameRingReader(segment.buffer, handle.shmId, handle.slotCount, slotByteLength, handle.nodeId),
|
|
643
|
+
lastUsedAt: now
|
|
638
644
|
};
|
|
639
645
|
this.rings.set(handle.shmId, ring);
|
|
646
|
+
this.sweepStaleRings(now, handle.shmId);
|
|
640
647
|
return ring;
|
|
641
648
|
} catch (err) {
|
|
642
649
|
this.logger?.warn("frame-ring reader: openSegment failed", { meta: {
|
|
@@ -646,6 +653,44 @@ var FrameRingReaderCache = class {
|
|
|
646
653
|
return null;
|
|
647
654
|
}
|
|
648
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Close (`munmap`) and evict every cached reader — except `keepShmId`, the
|
|
658
|
+
* one being looked up right now — whose segment stopped appearing in handles
|
|
659
|
+
* for longer than `staleReaderTtlMs`. Cheap: a no-op unless more than one
|
|
660
|
+
* segment is mapped (steady state holds exactly one per subscription), so the
|
|
661
|
+
* common per-read path does not even iterate.
|
|
662
|
+
*/
|
|
663
|
+
sweepStaleRings(now, keepShmId) {
|
|
664
|
+
if (this.rings.size <= 1) return;
|
|
665
|
+
for (const [shmId, ring] of this.rings) {
|
|
666
|
+
if (shmId === keepShmId) continue;
|
|
667
|
+
if (now - ring.lastUsedAt <= this.staleReaderTtlMs) continue;
|
|
668
|
+
this.logger?.debug("frame-ring reader: evicting stale mapping", { meta: {
|
|
669
|
+
shmId,
|
|
670
|
+
idleMs: now - ring.lastUsedAt
|
|
671
|
+
} });
|
|
672
|
+
this.closeRing(shmId, ring);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Unmap one cached segment and drop it from the map. The native `close()` is
|
|
677
|
+
* itself idempotent (see `native.ts`), and deleting from the map before the
|
|
678
|
+
* `munmap` keeps eviction idempotent even if `close()` throws — the reader is
|
|
679
|
+
* never revived. This only `munmap`s the reader's own view; it does NOT
|
|
680
|
+
* `shm_unlink` (the decoder that created the segment owns the name), which is
|
|
681
|
+
* exactly what lets the OS reclaim the backing memory once every view closes.
|
|
682
|
+
*/
|
|
683
|
+
closeRing(shmId, ring) {
|
|
684
|
+
this.rings.delete(shmId);
|
|
685
|
+
try {
|
|
686
|
+
ring.segment.close();
|
|
687
|
+
} catch (err) {
|
|
688
|
+
this.logger?.warn("frame-ring reader: segment close failed", { meta: {
|
|
689
|
+
shmId,
|
|
690
|
+
error: errMsg(err)
|
|
691
|
+
} });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
649
694
|
};
|
|
650
695
|
//#endregion
|
|
651
696
|
export { FrameRingReader, FrameRingReaderCache, FrameRingWriter, MAX_RING_SLOTS, MIN_RING_SLOTS, bytesPerPixel, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount, openSegment, unlinkSegment };
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|