dsh-homepage 0.1.4 → 0.1.5
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/docs/card-api.md +4 -2
- package/lib/cards/device.js +6 -0
- package/lib/client.js +64 -4
- package/lib/framework.part.js +58 -4
- package/lib/host/device.js +176 -53
- package/lib/index.js +40 -61
- package/package.json +1 -1
package/docs/card-api.md
CHANGED
|
@@ -80,7 +80,7 @@ including a built-in one — requires `override: true`, otherwise it throws.
|
|
|
80
80
|
| `effects` | `{outline, ripple, sheen, float, spotlight}` | all off | Animation defaults (see Effects). |
|
|
81
81
|
| `refreshMs` | number | `0` | Refresh hint for polling bodies; the card still owns its timer. |
|
|
82
82
|
| `expandable` / `draggable` / `resizable` | boolean | `true` | Turn that interaction off. |
|
|
83
|
-
| `onMount` / `onExpand` / `onCollapse` | `(api) => void` | `null` | Lifecycle callbacks. |
|
|
83
|
+
| `onMount` / `onExpand` / `onCollapse` | `(api) => void` | `null` | Lifecycle callbacks. `onMount` fires the first time the card is actually on the wall — a hidden card runs nothing until it is shown. |
|
|
84
84
|
| `tags` / `order` / `version` | string[] / number / string | `[]` / `0` / `null` | Manifest tags, initial position hint (drag order wins) and registrant version. |
|
|
85
85
|
| `dict` | `{zh, en}` | `null` | Card-local strings merged into the locale service. |
|
|
86
86
|
| `override` | boolean | `false` | Required to replace another plugin's card. |
|
|
@@ -105,7 +105,9 @@ including a built-in one — requires `override: true`, otherwise it throws.
|
|
|
105
105
|
|
|
106
106
|
Framework hooks are on `hooks`: `useStore()`, `useRemote(endpoint, args, {refreshMs, enabled})`
|
|
107
107
|
(returns `{data, error, busy, reload, at, failures}`), `useClock(ms)`, `useNoticeOnce(api, key, level, text)`
|
|
108
|
-
and `useAnnounce(api, key, notice)`.
|
|
108
|
+
and `useAnnounce(api, key, notice)`. A poll is scheduled one tick at a time with a little
|
|
109
|
+
jitter (about ±10%) so cards mounted together do not keep refreshing together, and
|
|
110
|
+
`enabled: false` (or `refreshMs: 0`) stops the timer entirely.
|
|
109
111
|
|
|
110
112
|
## Controls (`actions` / `detailActions`)
|
|
111
113
|
|
package/lib/cards/device.js
CHANGED
|
@@ -139,6 +139,12 @@
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* The polling interval, in ms, that the card's own control asks for.
|
|
142
|
+
*
|
|
143
|
+
* Five seconds is the original cadence and stays: what makes a fast poll
|
|
144
|
+
* cheap is the host, which reads CPU and memory in-process and refreshes
|
|
145
|
+
* the GPU/temperature probes on their own slower, cached schedule instead
|
|
146
|
+
* of paying for one on every request. A slower control is still offered for
|
|
147
|
+
* readers who want it.
|
|
142
148
|
* @param api - the card's API bag.
|
|
143
149
|
* @returns the interval in ms, at least one second.
|
|
144
150
|
*/
|
package/lib/client.js
CHANGED
|
@@ -575,6 +575,9 @@ window.__ModuleLoader__.load({
|
|
|
575
575
|
card: next,
|
|
576
576
|
previous: existing || null,
|
|
577
577
|
})
|
|
578
|
+
// A card registered after the wall booted is mounted only if it is on
|
|
579
|
+
// the wall; a hidden one waits until it is shown.
|
|
580
|
+
if (bootstrapped && store.hostReady && cardVisible(next)) mountCard(next)
|
|
578
581
|
return next
|
|
579
582
|
}
|
|
580
583
|
|
|
@@ -589,6 +592,7 @@ window.__ModuleLoader__.load({
|
|
|
589
592
|
registry.splice(registry.indexOf(card), 1)
|
|
590
593
|
delete registryById[id]
|
|
591
594
|
delete store.notifications[id]
|
|
595
|
+
delete mountedCards[id]
|
|
592
596
|
if (store.expanded === id) store.expanded = null
|
|
593
597
|
if (store.expanded === null) store.focusFrom = null
|
|
594
598
|
bump()
|
|
@@ -1310,14 +1314,35 @@ window.__ModuleLoader__.load({
|
|
|
1310
1314
|
}, [endpoint, argsKey])
|
|
1311
1315
|
|
|
1312
1316
|
React.useEffect(function () {
|
|
1317
|
+
if (settings.enabled === false) {
|
|
1318
|
+
alive.current = false
|
|
1319
|
+
return undefined
|
|
1320
|
+
}
|
|
1313
1321
|
alive.current = true
|
|
1314
|
-
if (settings.enabled === false) return undefined
|
|
1315
1322
|
void load(false)
|
|
1316
1323
|
if (!settings.refreshMs) return function () { alive.current = false }
|
|
1317
|
-
var
|
|
1324
|
+
var base = Math.max(1000, Number(settings.refreshMs) || 0)
|
|
1325
|
+
var timer = null
|
|
1326
|
+
// Re-arm one timer at a time with a little jitter. Cards that mounted
|
|
1327
|
+
// together would otherwise refresh together forever, and every peak is
|
|
1328
|
+
// paid by the host at once; ±10% spreads them without changing the
|
|
1329
|
+
// cadence a reader sees.
|
|
1330
|
+
var arm = function () {
|
|
1331
|
+
var delay = Math.round(base * (0.9 + Math.random() * 0.2))
|
|
1332
|
+
timer = setTimeout(function () {
|
|
1333
|
+
timer = null
|
|
1334
|
+
if (!alive.current) return
|
|
1335
|
+
void load(true)
|
|
1336
|
+
arm()
|
|
1337
|
+
}, delay)
|
|
1338
|
+
// A card test (or a headless host) must not be kept alive by a poll
|
|
1339
|
+
// it never sees fire; a browser timer has no unref and ignores this.
|
|
1340
|
+
if (timer && typeof timer.unref === 'function') timer.unref()
|
|
1341
|
+
}
|
|
1342
|
+
arm()
|
|
1318
1343
|
return function () {
|
|
1319
1344
|
alive.current = false
|
|
1320
|
-
|
|
1345
|
+
if (timer !== null) clearTimeout(timer)
|
|
1321
1346
|
}
|
|
1322
1347
|
}, [load, settings.refreshMs, settings.enabled])
|
|
1323
1348
|
|
|
@@ -1584,6 +1609,8 @@ window.__ModuleLoader__.load({
|
|
|
1584
1609
|
if (!visible) hidden.push(card.id)
|
|
1585
1610
|
if (!visible && store.expanded === card.id) collapseCard()
|
|
1586
1611
|
patchState({ hidden: hidden })
|
|
1612
|
+
// A card is mounted when it reaches the wall, not when it was defined.
|
|
1613
|
+
if (visible) mountCard(card)
|
|
1587
1614
|
}
|
|
1588
1615
|
|
|
1589
1616
|
function textureLayer(texture, card) {
|
|
@@ -2928,11 +2955,33 @@ window.__ModuleLoader__.load({
|
|
|
2928
2955
|
// -------------------------------------------------------------- bootstrap
|
|
2929
2956
|
|
|
2930
2957
|
var bootstrapped = false
|
|
2958
|
+
/** Cards whose `onMount` already ran; one entry per card id. */
|
|
2959
|
+
var mountedCards = Object.create(null)
|
|
2960
|
+
|
|
2961
|
+
/**
|
|
2962
|
+
* Run a card's `onMount` the first time it is actually on the wall.
|
|
2963
|
+
*
|
|
2964
|
+
* A hidden card is disabled: it is not rendered, it polls nothing, and it
|
|
2965
|
+
* must not get lifecycle work either. The old bootstrap ran every
|
|
2966
|
+
* registered card's `onMount` before the persisted visibility was known,
|
|
2967
|
+
* so a disabled card still armed whatever its hook armed.
|
|
2968
|
+
* @param card - a registered card.
|
|
2969
|
+
*/
|
|
2970
|
+
function mountCard(card) {
|
|
2971
|
+
if (mountedCards[card.id] === true) return
|
|
2972
|
+
mountedCards[card.id] = true
|
|
2973
|
+
runOnMount(card)
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
/** Mount every card the persisted layout puts on the wall. */
|
|
2977
|
+
function mountVisibleCards() {
|
|
2978
|
+
orderedCards().filter(cardVisible).forEach(mountCard)
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2931
2981
|
/** Load the persisted layout and install card hooks exactly once. */
|
|
2932
2982
|
function bootstrap() {
|
|
2933
2983
|
if (bootstrapped) return Promise.resolve()
|
|
2934
2984
|
bootstrapped = true
|
|
2935
|
-
registry.forEach(function (card) { runOnMount(card) })
|
|
2936
2985
|
return remoteCall('homepage/state', {}).then(function (persisted) {
|
|
2937
2986
|
if (persisted && typeof persisted === 'object') {
|
|
2938
2987
|
store.state = stateDocumentOf(persisted)
|
|
@@ -2946,6 +2995,11 @@ window.__ModuleLoader__.load({
|
|
|
2946
2995
|
store.hostReady = true
|
|
2947
2996
|
bump()
|
|
2948
2997
|
return null
|
|
2998
|
+
}).then(function (value) {
|
|
2999
|
+
// Only after the visibility document is in hand: a card the user hid
|
|
3000
|
+
// is not mounted and runs nothing.
|
|
3001
|
+
mountVisibleCards()
|
|
3002
|
+
return value
|
|
2949
3003
|
})
|
|
2950
3004
|
}
|
|
2951
3005
|
|
|
@@ -6222,6 +6276,12 @@ var STYLES = [
|
|
|
6222
6276
|
|
|
6223
6277
|
/**
|
|
6224
6278
|
* The polling interval, in ms, that the card's own control asks for.
|
|
6279
|
+
*
|
|
6280
|
+
* Five seconds is the original cadence and stays: what makes a fast poll
|
|
6281
|
+
* cheap is the host, which reads CPU and memory in-process and refreshes
|
|
6282
|
+
* the GPU/temperature probes on their own slower, cached schedule instead
|
|
6283
|
+
* of paying for one on every request. A slower control is still offered for
|
|
6284
|
+
* readers who want it.
|
|
6225
6285
|
* @param api - the card's API bag.
|
|
6226
6286
|
* @returns the interval in ms, at least one second.
|
|
6227
6287
|
*/
|
package/lib/framework.part.js
CHANGED
|
@@ -563,6 +563,9 @@ window.__ModuleLoader__.load({
|
|
|
563
563
|
card: next,
|
|
564
564
|
previous: existing || null,
|
|
565
565
|
})
|
|
566
|
+
// A card registered after the wall booted is mounted only if it is on
|
|
567
|
+
// the wall; a hidden one waits until it is shown.
|
|
568
|
+
if (bootstrapped && store.hostReady && cardVisible(next)) mountCard(next)
|
|
566
569
|
return next
|
|
567
570
|
}
|
|
568
571
|
|
|
@@ -577,6 +580,7 @@ window.__ModuleLoader__.load({
|
|
|
577
580
|
registry.splice(registry.indexOf(card), 1)
|
|
578
581
|
delete registryById[id]
|
|
579
582
|
delete store.notifications[id]
|
|
583
|
+
delete mountedCards[id]
|
|
580
584
|
if (store.expanded === id) store.expanded = null
|
|
581
585
|
if (store.expanded === null) store.focusFrom = null
|
|
582
586
|
bump()
|
|
@@ -1298,14 +1302,35 @@ window.__ModuleLoader__.load({
|
|
|
1298
1302
|
}, [endpoint, argsKey])
|
|
1299
1303
|
|
|
1300
1304
|
React.useEffect(function () {
|
|
1305
|
+
if (settings.enabled === false) {
|
|
1306
|
+
alive.current = false
|
|
1307
|
+
return undefined
|
|
1308
|
+
}
|
|
1301
1309
|
alive.current = true
|
|
1302
|
-
if (settings.enabled === false) return undefined
|
|
1303
1310
|
void load(false)
|
|
1304
1311
|
if (!settings.refreshMs) return function () { alive.current = false }
|
|
1305
|
-
var
|
|
1312
|
+
var base = Math.max(1000, Number(settings.refreshMs) || 0)
|
|
1313
|
+
var timer = null
|
|
1314
|
+
// Re-arm one timer at a time with a little jitter. Cards that mounted
|
|
1315
|
+
// together would otherwise refresh together forever, and every peak is
|
|
1316
|
+
// paid by the host at once; ±10% spreads them without changing the
|
|
1317
|
+
// cadence a reader sees.
|
|
1318
|
+
var arm = function () {
|
|
1319
|
+
var delay = Math.round(base * (0.9 + Math.random() * 0.2))
|
|
1320
|
+
timer = setTimeout(function () {
|
|
1321
|
+
timer = null
|
|
1322
|
+
if (!alive.current) return
|
|
1323
|
+
void load(true)
|
|
1324
|
+
arm()
|
|
1325
|
+
}, delay)
|
|
1326
|
+
// A card test (or a headless host) must not be kept alive by a poll
|
|
1327
|
+
// it never sees fire; a browser timer has no unref and ignores this.
|
|
1328
|
+
if (timer && typeof timer.unref === 'function') timer.unref()
|
|
1329
|
+
}
|
|
1330
|
+
arm()
|
|
1306
1331
|
return function () {
|
|
1307
1332
|
alive.current = false
|
|
1308
|
-
|
|
1333
|
+
if (timer !== null) clearTimeout(timer)
|
|
1309
1334
|
}
|
|
1310
1335
|
}, [load, settings.refreshMs, settings.enabled])
|
|
1311
1336
|
|
|
@@ -1572,6 +1597,8 @@ window.__ModuleLoader__.load({
|
|
|
1572
1597
|
if (!visible) hidden.push(card.id)
|
|
1573
1598
|
if (!visible && store.expanded === card.id) collapseCard()
|
|
1574
1599
|
patchState({ hidden: hidden })
|
|
1600
|
+
// A card is mounted when it reaches the wall, not when it was defined.
|
|
1601
|
+
if (visible) mountCard(card)
|
|
1575
1602
|
}
|
|
1576
1603
|
|
|
1577
1604
|
function textureLayer(texture, card) {
|
|
@@ -2916,11 +2943,33 @@ window.__ModuleLoader__.load({
|
|
|
2916
2943
|
// -------------------------------------------------------------- bootstrap
|
|
2917
2944
|
|
|
2918
2945
|
var bootstrapped = false
|
|
2946
|
+
/** Cards whose `onMount` already ran; one entry per card id. */
|
|
2947
|
+
var mountedCards = Object.create(null)
|
|
2948
|
+
|
|
2949
|
+
/**
|
|
2950
|
+
* Run a card's `onMount` the first time it is actually on the wall.
|
|
2951
|
+
*
|
|
2952
|
+
* A hidden card is disabled: it is not rendered, it polls nothing, and it
|
|
2953
|
+
* must not get lifecycle work either. The old bootstrap ran every
|
|
2954
|
+
* registered card's `onMount` before the persisted visibility was known,
|
|
2955
|
+
* so a disabled card still armed whatever its hook armed.
|
|
2956
|
+
* @param card - a registered card.
|
|
2957
|
+
*/
|
|
2958
|
+
function mountCard(card) {
|
|
2959
|
+
if (mountedCards[card.id] === true) return
|
|
2960
|
+
mountedCards[card.id] = true
|
|
2961
|
+
runOnMount(card)
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
/** Mount every card the persisted layout puts on the wall. */
|
|
2965
|
+
function mountVisibleCards() {
|
|
2966
|
+
orderedCards().filter(cardVisible).forEach(mountCard)
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2919
2969
|
/** Load the persisted layout and install card hooks exactly once. */
|
|
2920
2970
|
function bootstrap() {
|
|
2921
2971
|
if (bootstrapped) return Promise.resolve()
|
|
2922
2972
|
bootstrapped = true
|
|
2923
|
-
registry.forEach(function (card) { runOnMount(card) })
|
|
2924
2973
|
return remoteCall('homepage/state', {}).then(function (persisted) {
|
|
2925
2974
|
if (persisted && typeof persisted === 'object') {
|
|
2926
2975
|
store.state = stateDocumentOf(persisted)
|
|
@@ -2934,6 +2983,11 @@ window.__ModuleLoader__.load({
|
|
|
2934
2983
|
store.hostReady = true
|
|
2935
2984
|
bump()
|
|
2936
2985
|
return null
|
|
2986
|
+
}).then(function (value) {
|
|
2987
|
+
// Only after the visibility document is in hand: a card the user hid
|
|
2988
|
+
// is not mounted and runs nothing.
|
|
2989
|
+
mountVisibleCards()
|
|
2990
|
+
return value
|
|
2937
2991
|
})
|
|
2938
2992
|
}
|
|
2939
2993
|
|
package/lib/host/device.js
CHANGED
|
@@ -14,19 +14,25 @@
|
|
|
14
14
|
* - CPU usage needs two readings, so the previous `os.cpus()` snapshot is kept
|
|
15
15
|
* in module state. The first call of a process waits ~250 ms and reads again
|
|
16
16
|
* rather than reporting a meaningless 0;
|
|
17
|
-
* - a sample taken less than a second ago is memoized,
|
|
18
|
-
*
|
|
17
|
+
* - a sample taken less than a second ago is memoized, and a sample still in
|
|
18
|
+
* flight is shared by every caller, because several subscribers poll the
|
|
19
|
+
* same endpoint at once;
|
|
19
20
|
* - the temperature probe costs a subprocess, so it is cached for
|
|
20
|
-
* `temperatureTtlMs
|
|
21
|
-
* skips it outright, which is what the periodic history sampler passes;
|
|
21
|
+
* `temperatureTtlMs`. `temperature: false` skips it outright;
|
|
22
22
|
* - the GPU counters are the expensive read: Windows needs one PowerShell
|
|
23
23
|
* spawn and Linux needs one `nvidia-smi` spawn when no sysfs card answers,
|
|
24
|
-
* so the verdict is cached for `gpuTtlMs
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
28
|
-
* -
|
|
29
|
-
* the
|
|
24
|
+
* so the verdict is cached for `gpuTtlMs`, and a failed verdict for
|
|
25
|
+
* `GPU_FAILURE_TTL_MS`. A Linux machine whose amdgpu publishes
|
|
26
|
+
* `gpu_busy_percent` reads plain files and spawns nothing at all. `gpu: false`
|
|
27
|
+
* skips the block outright;
|
|
28
|
+
* - a stale temperature or GPU cache is served immediately while its probe
|
|
29
|
+
* refreshes in the background; only the first request (a cold cache) waits.
|
|
30
|
+
* That is what keeps a fast poll as cheap as a performance panel: CPU,
|
|
31
|
+
* memory and disk are read in-process, and the subprocess blocks are
|
|
32
|
+
* refreshed on their own slower cadence;
|
|
33
|
+
* - the plugin arms no background sampler: every sample is on demand, so an
|
|
34
|
+
* installed page nobody opened probes nothing. `startDeviceSampler()` exists
|
|
35
|
+
* for a caller that wants the cheap history path, and it never spawns;
|
|
30
36
|
* - history is an in-memory ring buffer per process only — it is deliberately
|
|
31
37
|
* NOT persisted anywhere;
|
|
32
38
|
* - every source is injectable through `options`, which is what lets the unit
|
|
@@ -58,7 +64,17 @@ const WINDOWS_PLATFORMS = new Set(['win32', 'windows', 'win'])
|
|
|
58
64
|
const PERMISSION_CODES = new Set(['EACCES', 'EPERM'])
|
|
59
65
|
|
|
60
66
|
/** How long one GPU probe's verdict is reused; that probe costs a subprocess. */
|
|
61
|
-
const GPU_TTL_MS =
|
|
67
|
+
const GPU_TTL_MS = 30000
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* How long a failed GPU verdict is reused.
|
|
71
|
+
*
|
|
72
|
+
* A machine whose counters are disabled, unreadable or slow kept paying a
|
|
73
|
+
* PowerShell spawn on every poll. Reusing the failure for a minute turns a
|
|
74
|
+
* per-poll subprocess into one a minute without hiding a recovered machine for
|
|
75
|
+
* long.
|
|
76
|
+
*/
|
|
77
|
+
const GPU_FAILURE_TTL_MS = 60000
|
|
62
78
|
|
|
63
79
|
/**
|
|
64
80
|
* How long the GPU probe may run before it is killed.
|
|
@@ -165,8 +181,16 @@ let lastSample = null
|
|
|
165
181
|
/** Cached temperature verdict plus the epoch ms it was taken at. */
|
|
166
182
|
let temperatureCache = null
|
|
167
183
|
|
|
168
|
-
/** Cached GPU block plus the epoch ms the probe
|
|
184
|
+
/** Cached GPU block plus the epoch ms the probe finished at. */
|
|
169
185
|
let gpuCache = null
|
|
186
|
+
/** The GPU probe currently in flight, shared by concurrent readers. */
|
|
187
|
+
let gpuPending = null
|
|
188
|
+
/** The temperature probe currently in flight, shared by concurrent readers. */
|
|
189
|
+
let temperaturePending = null
|
|
190
|
+
/** The sample currently in flight, shared by concurrent readers. */
|
|
191
|
+
let samplePending = null
|
|
192
|
+
/** Bumped by `resetDeviceCache()`; a stale probe must not refill a reset cache. */
|
|
193
|
+
let cacheEpoch = 0
|
|
170
194
|
/** Appended per real sample; in memory for this process only. */
|
|
171
195
|
let history = []
|
|
172
196
|
/** Set once the first call has paid for its second CPU reading. */
|
|
@@ -721,16 +745,53 @@ async function readTemperature(deps) {
|
|
|
721
745
|
}
|
|
722
746
|
|
|
723
747
|
/**
|
|
724
|
-
*
|
|
748
|
+
* Run one temperature probe, sharing it with any caller already waiting.
|
|
749
|
+
*
|
|
750
|
+
* The cache is stamped when the probe finishes rather than when it started, so
|
|
751
|
+
* a slow subprocess is not immediately stale. `resetDeviceCache()` bumps the
|
|
752
|
+
* epoch, which keeps a probe that lands after a reset from repopulating it.
|
|
753
|
+
* @param deps - the resolved dependency bag.
|
|
754
|
+
* @returns `{ celsius, source, reason }`.
|
|
755
|
+
*/
|
|
756
|
+
async function probeTemperature(deps) {
|
|
757
|
+
if (temperaturePending !== null) return temperaturePending
|
|
758
|
+
const epoch = cacheEpoch
|
|
759
|
+
const run = (async () => {
|
|
760
|
+
const value = await readTemperature(deps)
|
|
761
|
+
if (epoch === cacheEpoch) {
|
|
762
|
+
temperatureCache = { at: deps.now(), value: value, ttlMs: deps.temperatureTtlMs }
|
|
763
|
+
}
|
|
764
|
+
return value
|
|
765
|
+
})()
|
|
766
|
+
temperaturePending = run
|
|
767
|
+
try {
|
|
768
|
+
return await run
|
|
769
|
+
} finally {
|
|
770
|
+
if (temperaturePending === run) temperaturePending = null
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* The temperature block for one sample.
|
|
776
|
+
*
|
|
777
|
+
* A performance panel must stay cheap at a fast cadence, so this never blocks a
|
|
778
|
+
* poll on the subprocess probe:
|
|
779
|
+
* - a warm cache answers directly;
|
|
780
|
+
* - a stale cache answers with its last value at once and refreshes in the
|
|
781
|
+
* background — the next poll picks the new reading up;
|
|
782
|
+
* - a cold cache (the first request after start) waits once, so the card is
|
|
783
|
+
* never blank.
|
|
725
784
|
* @param deps - the resolved dependency bag.
|
|
726
785
|
* @returns `{ celsius, source, reason }`.
|
|
727
786
|
*/
|
|
728
787
|
async function temperature(deps) {
|
|
729
788
|
const at = deps.now()
|
|
730
|
-
if (temperatureCache && at - temperatureCache.at <
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
return
|
|
789
|
+
if (temperatureCache && at - temperatureCache.at < temperatureCache.ttlMs) return temperatureCache.value
|
|
790
|
+
const stale = temperatureCache ? temperatureCache.value : null
|
|
791
|
+
const run = probeTemperature(deps)
|
|
792
|
+
if (stale === null) return run
|
|
793
|
+
run.catch(() => {})
|
|
794
|
+
return stale
|
|
734
795
|
}
|
|
735
796
|
|
|
736
797
|
// -------------------------------------------------------------------- gpu
|
|
@@ -1262,16 +1323,56 @@ async function readGpu(deps) {
|
|
|
1262
1323
|
}
|
|
1263
1324
|
|
|
1264
1325
|
/**
|
|
1265
|
-
*
|
|
1326
|
+
* Run one GPU probe, sharing it with any caller already waiting.
|
|
1327
|
+
*
|
|
1328
|
+
* The cache is stamped when the probe finishes, and a failed verdict gets the
|
|
1329
|
+
* longer `GPU_FAILURE_TTL_MS` so a broken counter provider is not retried at
|
|
1330
|
+
* poll speed. The epoch guard keeps a late probe out of a reset cache.
|
|
1331
|
+
* @param deps - the resolved dependency bag.
|
|
1332
|
+
* @returns the `gpu` block; never rejects.
|
|
1333
|
+
*/
|
|
1334
|
+
async function probeGpu(deps) {
|
|
1335
|
+
if (gpuPending !== null) return gpuPending
|
|
1336
|
+
const epoch = cacheEpoch
|
|
1337
|
+
const run = (async () => {
|
|
1338
|
+
const value = await readGpu(deps)
|
|
1339
|
+
if (epoch === cacheEpoch) {
|
|
1340
|
+
const failed = !value || value.usage === null
|
|
1341
|
+
gpuCache = {
|
|
1342
|
+
at: deps.now(),
|
|
1343
|
+
value: value,
|
|
1344
|
+
ttlMs: failed ? GPU_FAILURE_TTL_MS : deps.gpuTtlMs,
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return value
|
|
1348
|
+
})()
|
|
1349
|
+
gpuPending = run
|
|
1350
|
+
try {
|
|
1351
|
+
return await run
|
|
1352
|
+
} finally {
|
|
1353
|
+
if (gpuPending === run) gpuPending = null
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* The GPU block for one sample.
|
|
1359
|
+
*
|
|
1360
|
+
* The probe is the most expensive read the surface makes, so a poll never waits
|
|
1361
|
+
* for a refresh: a warm cache answers directly, a stale cache answers with its
|
|
1362
|
+
* last value while the probe runs in the background, and only a cold cache (the
|
|
1363
|
+
* first request) waits. That is what lets the card poll quickly for CPU and
|
|
1364
|
+
* memory without making the GPU a per-poll subprocess.
|
|
1266
1365
|
* @param deps - the resolved dependency bag.
|
|
1267
1366
|
* @returns the `gpu` block; never rejects.
|
|
1268
1367
|
*/
|
|
1269
1368
|
async function gpuReading(deps) {
|
|
1270
1369
|
const at = deps.now()
|
|
1271
|
-
if (gpuCache && at - gpuCache.at <
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
return
|
|
1370
|
+
if (gpuCache && at - gpuCache.at < gpuCache.ttlMs) return gpuCache.value
|
|
1371
|
+
const stale = gpuCache ? gpuCache.value : null
|
|
1372
|
+
const run = probeGpu(deps)
|
|
1373
|
+
if (stale === null) return run
|
|
1374
|
+
run.catch(() => {})
|
|
1375
|
+
return stale
|
|
1275
1376
|
}
|
|
1276
1377
|
|
|
1277
1378
|
// ------------------------------------------------------------------ cache
|
|
@@ -1282,11 +1383,15 @@ async function gpuReading(deps) {
|
|
|
1282
1383
|
* @returns nothing.
|
|
1283
1384
|
*/
|
|
1284
1385
|
export function resetDeviceCache() {
|
|
1386
|
+
cacheEpoch += 1
|
|
1285
1387
|
prevTimes = null
|
|
1286
1388
|
prevAt = null
|
|
1287
1389
|
lastSample = null
|
|
1288
1390
|
temperatureCache = null
|
|
1391
|
+
temperaturePending = null
|
|
1289
1392
|
gpuCache = null
|
|
1393
|
+
gpuPending = null
|
|
1394
|
+
samplePending = null
|
|
1290
1395
|
history = []
|
|
1291
1396
|
firstReadDone = false
|
|
1292
1397
|
}
|
|
@@ -1314,7 +1419,7 @@ function resolve(options) {
|
|
|
1314
1419
|
sleep: typeof input.sleep === 'function' ? input.sleep : defaultSleep,
|
|
1315
1420
|
temperatureTtlMs: toFinite(input.temperatureTtlMs) !== null && input.temperatureTtlMs >= 0
|
|
1316
1421
|
? input.temperatureTtlMs
|
|
1317
|
-
:
|
|
1422
|
+
: 120000,
|
|
1318
1423
|
temperature: input.temperature !== false,
|
|
1319
1424
|
gpuTtlMs: toFinite(input.gpuTtlMs) !== null && input.gpuTtlMs >= 0
|
|
1320
1425
|
? input.gpuTtlMs
|
|
@@ -1361,8 +1466,15 @@ async function gather(deps) {
|
|
|
1361
1466
|
const previousAt = prevAt
|
|
1362
1467
|
prevAt = at
|
|
1363
1468
|
const memory = readMemory(deps.os)
|
|
1364
|
-
|
|
1365
|
-
|
|
1469
|
+
// The two hardware blocks are independent: a cold start pays for them in
|
|
1470
|
+
// parallel rather than one after the other. Once warm, both resolve from
|
|
1471
|
+
// cache in the same tick, so a fast poll never waits on a subprocess.
|
|
1472
|
+
const blocks = await Promise.all([
|
|
1473
|
+
deps.temperature ? temperature(deps) : { celsius: null, source: null, reason: 'skipped' },
|
|
1474
|
+
deps.gpu ? gpuReading(deps) : emptyGpu('skipped'),
|
|
1475
|
+
])
|
|
1476
|
+
const thermal = blocks[0]
|
|
1477
|
+
const gpu = blocks[1]
|
|
1366
1478
|
const drive = systemDrive(deps.platform)
|
|
1367
1479
|
|
|
1368
1480
|
const cpu = {
|
|
@@ -1416,40 +1528,51 @@ async function gather(deps) {
|
|
|
1416
1528
|
*
|
|
1417
1529
|
* Never rejects: every source that fails degrades to `null`, to a `reason`
|
|
1418
1530
|
* code, or to zeros, and the rest of the document still comes back. A call
|
|
1419
|
-
* inside the one-second dedupe window returns the memoized document unchanged
|
|
1531
|
+
* inside the one-second dedupe window returns the memoized document unchanged,
|
|
1532
|
+
* and a call made while a sample is still gathering shares that sample instead
|
|
1533
|
+
* of starting a second set of probes.
|
|
1420
1534
|
* @param options - injectable sources and limits; see the module docblock.
|
|
1421
1535
|
* @returns the sample document.
|
|
1422
1536
|
*/
|
|
1423
1537
|
export async function sampleDevice(options = {}) {
|
|
1424
1538
|
const deps = resolve(options)
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1539
|
+
const at = deps.now()
|
|
1540
|
+
if (lastSample && at - lastSample.at < MIN_SAMPLE_INTERVAL_MS) return lastSample
|
|
1541
|
+
if (samplePending !== null) return samplePending
|
|
1542
|
+
const run = (async () => {
|
|
1543
|
+
try {
|
|
1544
|
+
const sample = await gather(deps)
|
|
1545
|
+
lastSample = sample
|
|
1546
|
+
return sample
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
return {
|
|
1549
|
+
at: deps.now(),
|
|
1550
|
+
sampleSeconds: 0,
|
|
1551
|
+
cpu: { usage: 0, cores: [], count: 0, model: '', speedMhz: 0, loadavg: null },
|
|
1552
|
+
memory: { total: 0, free: 0, used: 0, usage: 0, swapTotal: 0, swapFree: 0, swapUsed: 0 },
|
|
1553
|
+
temperature: { celsius: null, source: null, reason: 'error' },
|
|
1554
|
+
gpu: emptyGpu('error'),
|
|
1555
|
+
disk: null,
|
|
1556
|
+
system: {
|
|
1557
|
+
platform: deps.platform,
|
|
1558
|
+
arch: deps.platform,
|
|
1559
|
+
release: '',
|
|
1560
|
+
hostname: '',
|
|
1561
|
+
uptime: 0,
|
|
1562
|
+
node: process.version,
|
|
1563
|
+
cpuModel: '',
|
|
1564
|
+
drive: null,
|
|
1565
|
+
},
|
|
1566
|
+
history: history.slice(),
|
|
1567
|
+
error: error && error.message ? String(error.message) : 'sample failed',
|
|
1568
|
+
}
|
|
1452
1569
|
}
|
|
1570
|
+
})()
|
|
1571
|
+
samplePending = run
|
|
1572
|
+
try {
|
|
1573
|
+
return await run
|
|
1574
|
+
} finally {
|
|
1575
|
+
if (samplePending === run) samplePending = null
|
|
1453
1576
|
}
|
|
1454
1577
|
}
|
|
1455
1578
|
|
package/lib/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import { createActions } from './host/actions.js'
|
|
|
25
25
|
import { versionFromEntry } from './host/audit.js'
|
|
26
26
|
import { createAuditRunner } from './host/audit-runner.js'
|
|
27
27
|
import { createCollector } from './host/collector.js'
|
|
28
|
-
import { sampleDevice
|
|
28
|
+
import { sampleDevice } from './host/device.js'
|
|
29
29
|
import { sampleDshRelease } from './host/dsh.js'
|
|
30
30
|
import { defaultInstallRoot, listInstalled, startInstall } from './host/dsh-install.js'
|
|
31
31
|
import { createHygiene } from './host/hygiene.js'
|
|
@@ -127,20 +127,6 @@ const MICRO = 1000000
|
|
|
127
127
|
const DAY_KEEP = 180
|
|
128
128
|
const HISTORY_DAYS = 21
|
|
129
129
|
const MIN_SAMPLE_INTERVAL_MS = 15000
|
|
130
|
-
/** Delay before the start-up observation, long enough for the account service to mount. */
|
|
131
|
-
const BOOT_SAMPLE_DELAY_MS = 4000
|
|
132
|
-
/**
|
|
133
|
-
* Background observation cadence. The ledger's per-day spend is the drop between
|
|
134
|
-
* the first and the latest sample of the day, so a day nobody looks at still
|
|
135
|
-
* needs more than one point. Thirty minutes is 48 free reads a day — enough to
|
|
136
|
-
* show when the spending happened without hammering the Platform.
|
|
137
|
-
*/
|
|
138
|
-
const BACKGROUND_SAMPLE_MS = 30 * 60 * 1000
|
|
139
|
-
// The device card's history only exists while something samples it. A tick this
|
|
140
|
-
// slow costs one `os.cpus()` read and keeps a usable window in front of whoever
|
|
141
|
-
// opens the card; the tick deliberately does not touch the temperature, which
|
|
142
|
-
// would shell out to the system on a timer nobody is watching.
|
|
143
|
-
const DEVICE_SAMPLE_MS = 15000
|
|
144
130
|
|
|
145
131
|
// --------------------------------------------------------------- storage
|
|
146
132
|
|
|
@@ -558,6 +544,13 @@ async function loadLedger() {
|
|
|
558
544
|
return value
|
|
559
545
|
}
|
|
560
546
|
|
|
547
|
+
/**
|
|
548
|
+
* The account read currently in flight, shared by every concurrent on-demand
|
|
549
|
+
* overview. A page that is closed never reaches this module: the plugin arms no
|
|
550
|
+
* background account timer, so an idle instance performs no Platform reads.
|
|
551
|
+
*/
|
|
552
|
+
let balanceReadPending = null
|
|
553
|
+
|
|
561
554
|
/**
|
|
562
555
|
* Sample the account, fold the result into the ledger and persist it.
|
|
563
556
|
* @param ctx - plugin context.
|
|
@@ -565,22 +558,34 @@ async function loadLedger() {
|
|
|
565
558
|
* @returns the read outcome, with the ledger attached.
|
|
566
559
|
*/
|
|
567
560
|
async function sample(ctx, force) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (!force &&
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
ledger.
|
|
579
|
-
ledger.
|
|
580
|
-
|
|
561
|
+
// Two card instances asking at the same moment (the wall preview and the
|
|
562
|
+
// expanded page) share one read instead of spending two.
|
|
563
|
+
if (!force && balanceReadPending !== null) return balanceReadPending
|
|
564
|
+
const run = (async () => {
|
|
565
|
+
const ledger = await loadLedger()
|
|
566
|
+
const now = Date.now()
|
|
567
|
+
if (!force && now - Number(ledger.lastAttemptAt || 0) < MIN_SAMPLE_INTERVAL_MS) {
|
|
568
|
+
return { ledger: ledger, read: ledger.lastOutcome || null, sampled: false, at: Number(ledger.lastAttemptAt || 0) }
|
|
569
|
+
}
|
|
570
|
+
const read = await readBalance(ctx)
|
|
571
|
+
ledger.lastAttemptAt = now
|
|
572
|
+
ledger.lastOutcome = { ok: read.ok, reason: read.reason || null, message: read.message || null, view: read.view || null }
|
|
573
|
+
if (read.ok) {
|
|
574
|
+
applySample(ledger, read.wallets, now)
|
|
575
|
+
ledger.wallet = read.wallets
|
|
576
|
+
ledger.bonusWallets = read.bonusWallets
|
|
577
|
+
ledger.walletAt = now
|
|
578
|
+
}
|
|
579
|
+
try { await writeJson(ledgerPath(), ledger) } catch { /* a failed cache write must not fail the read */ }
|
|
580
|
+
return { ledger: ledger, read: read, sampled: true, at: now }
|
|
581
|
+
})()
|
|
582
|
+
if (force) return run
|
|
583
|
+
balanceReadPending = run
|
|
584
|
+
try {
|
|
585
|
+
return await run
|
|
586
|
+
} finally {
|
|
587
|
+
if (balanceReadPending === run) balanceReadPending = null
|
|
581
588
|
}
|
|
582
|
-
try { await writeJson(ledgerPath(), ledger) } catch { /* a failed cache write must not fail the read */ }
|
|
583
|
-
return { ledger: ledger, read: read, sampled: true, at: now }
|
|
584
589
|
}
|
|
585
590
|
|
|
586
591
|
/**
|
|
@@ -2125,37 +2130,11 @@ export function apply(ctx, config) {
|
|
|
2125
2130
|
}
|
|
2126
2131
|
}
|
|
2127
2132
|
|
|
2128
|
-
//
|
|
2129
|
-
//
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2132
|
-
// so
|
|
2133
|
-
// recorded in the ledger rather than thrown at start-up.
|
|
2134
|
-
ctx.effect(() => {
|
|
2135
|
-
const take = () => { sample(ctx, false).catch(() => {}) }
|
|
2136
|
-
const timer = service(ctx, 'timer')
|
|
2137
|
-
if (timer && typeof timer.timeout === 'function' && typeof timer.interval === 'function') {
|
|
2138
|
-
const stopTimeout = timer.timeout(take, BOOT_SAMPLE_DELAY_MS)
|
|
2139
|
-
const stopInterval = timer.interval(take, BACKGROUND_SAMPLE_MS)
|
|
2140
|
-
return () => {
|
|
2141
|
-
if (typeof stopTimeout === 'function') stopTimeout()
|
|
2142
|
-
if (typeof stopInterval === 'function') stopInterval()
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
const timeout = setTimeout(take, BOOT_SAMPLE_DELAY_MS)
|
|
2146
|
-
const interval = setInterval(take, BACKGROUND_SAMPLE_MS)
|
|
2147
|
-
return () => { clearTimeout(timeout); clearInterval(interval) }
|
|
2148
|
-
}, name + ': balance sampling')
|
|
2149
|
-
|
|
2150
|
-
// Keep the device card's history warm, so the chart has a window the moment
|
|
2151
|
-
// somebody opens it. The tick is deliberately cheap: two `os` reads and never
|
|
2152
|
-
// a temperature probe, which is cached and only runs while the card polls.
|
|
2153
|
-
ctx.effect(() => {
|
|
2154
|
-
const take = () => { sampleDevice({ temperature: false }).catch(() => {}) }
|
|
2155
|
-
const timer = service(ctx, 'timer')
|
|
2156
|
-
if (timer && typeof timer.interval === 'function') return timer.interval(take, DEVICE_SAMPLE_MS)
|
|
2157
|
-
return startDeviceSampler({ intervalMs: DEVICE_SAMPLE_MS })
|
|
2158
|
-
}, name + ': device sampling')
|
|
2133
|
+
// No background sampling lives past this point. The account ledger and the
|
|
2134
|
+
// device history are filled only by an on-demand read (an open card or the
|
|
2135
|
+
// model-facing tool), so an installed-but-unused plugin runs nothing: no
|
|
2136
|
+
// Platform requests, no subprocesses, no timers. Disabled cards never mount,
|
|
2137
|
+
// so they never call an endpoint either.
|
|
2159
2138
|
}
|
|
2160
2139
|
|
|
2161
2140
|
/** @returns one error carrying the plugin's own prefix, without doubling it. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-homepage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "DeepSeek Harness plugin: a personal home page in the sidebar for a DSH instance. Review a plugin before installing it, and read sessions, projects, tool usage, device, storage, DSH cores, Skills and cost from one composable, registrable card wall.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|