pinokiod 8.0.54 → 8.0.55

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,9 +1,20 @@
1
1
  const fs = require("fs")
2
+ const os = require("os")
2
3
  const path = require("path")
3
- const { SIZE_THRESHOLD } = require("./constants")
4
+ const {
5
+ DIR_CONCURRENCY,
6
+ ENTRY_BATCH_SIZE,
7
+ HASH_INACTIVITY_MS,
8
+ HASH_PROGRESS_INTERVAL_MS,
9
+ HASH_READ_SIZE,
10
+ SIZE_THRESHOLD,
11
+ STAT_CONCURRENCY
12
+ } = require("./constants")
4
13
 
5
14
  const COMPLETE_PHASES = new Set(["complete", "completed_with_exclusions"])
6
15
  const STOP_SETTLE_MS = 3000
16
+ const SCAN_PROGRESS_INTERVAL_MS = 5000
17
+ const SCAN_STALL_THRESHOLD_MS = 30000
7
18
  const isMissing = (error) => !!(error &&
8
19
  (error.code === "ENOENT" || error.code === "ENOTDIR"))
9
20
 
@@ -34,6 +45,10 @@ class AutomaticScans {
34
45
  this.waitingFor = null
35
46
  this.listeners = new Set()
36
47
  this.appTransitions = new Map()
48
+ this.scanDiagnostics = null
49
+ this.scanProgressIntervalMs = SCAN_PROGRESS_INTERVAL_MS
50
+ this.scanStallThresholdMs = SCAN_STALL_THRESHOLD_MS
51
+ this.clock = () => Date.now()
37
52
  }
38
53
 
39
54
  log(event, details = {}) {
@@ -49,6 +64,345 @@ class AutomaticScans {
49
64
  console.log(`[Vault Automatic Scan] ${JSON.stringify(record)}`)
50
65
  }
51
66
 
67
+ diagnosticMarker(scan) {
68
+ return JSON.stringify([
69
+ scan.phase,
70
+ scan.stage,
71
+ scan.current_directory,
72
+ scan.anchor_files,
73
+ scan.anchor_bytes,
74
+ scan.walk_batches,
75
+ scan.dirs,
76
+ scan.files,
77
+ scan.bytes_total,
78
+ scan.comparison_batches,
79
+ scan.comparison_files_verified,
80
+ scan.candidates,
81
+ scan.hash_work_files,
82
+ scan.hash_work_bytes,
83
+ scan.hash_files_completed,
84
+ scan.hash_bytes_completed,
85
+ scan.hash_total,
86
+ scan.hashed,
87
+ scan.hash_bytes,
88
+ scan.current_file,
89
+ scan.current_file_role,
90
+ scan.current_file_bytes,
91
+ scan.current_file_size,
92
+ scan.inode_reuses,
93
+ scan.unstable_hashes,
94
+ scan.hash_failures,
95
+ Array.isArray(scan.exclusions) ? scan.exclusions.length : 0
96
+ ])
97
+ }
98
+
99
+ diagnosticCounters(scan) {
100
+ const currentFileBytes = Math.max(
101
+ 0, Number(scan.current_file_bytes) || 0)
102
+ return {
103
+ files: Math.max(0, Number(scan.files) || 0),
104
+ discoveryBytes: Math.max(0, Number(scan.bytes_total) || 0),
105
+ hashBytes: Math.max(0,
106
+ Number(scan.hash_bytes_completed) || 0) + currentFileBytes
107
+ }
108
+ }
109
+
110
+ diagnosticDetails(monitor, scan, now) {
111
+ const started = Number(scan.started) || monitor.startedAt
112
+ const stageStarted = Number(scan.stage_started) || started
113
+ const lastProgress = Number(scan.last_progress_at) || stageStarted
114
+ const preview = scan.preview || {}
115
+ const currentFileSize = Math.max(
116
+ 0, Number(scan.current_file_size) || 0)
117
+ const currentFileBytes = Math.min(currentFileSize || Infinity, Math.max(
118
+ 0, Number(scan.current_file_bytes) || 0))
119
+ const hashWorkBytes = Math.max(
120
+ 0, Number(scan.hash_work_bytes) || 0)
121
+ const hashProgressBytes = Math.min(hashWorkBytes || Infinity, Math.max(
122
+ 0, Number(scan.hash_bytes_completed) || 0) + currentFileBytes)
123
+ const details = {
124
+ app: monitor.app,
125
+ scope_id: monitor.scopeId,
126
+ phase: scan.phase || "unknown",
127
+ stage: scan.stage || scan.phase || "unknown",
128
+ previous_stage: scan.previous_stage,
129
+ previous_stage_duration_ms: scan.previous_stage_duration_ms,
130
+ active: !!scan.active,
131
+ pending: !!scan.pending,
132
+ elapsed_ms: Math.max(0, now - started),
133
+ stage_elapsed_ms: Math.max(0, now - stageStarted),
134
+ progress_age_ms: Math.max(0, now - lastProgress),
135
+ progress_unchanged_ms: Math.max(0, now - monitor.lastChangedAt),
136
+ current_directory: scan.current_directory,
137
+ anchor_files: Math.max(0, Number(scan.anchor_files) || 0),
138
+ anchor_bytes: Math.max(0, Number(scan.anchor_bytes) || 0),
139
+ walk_batches: Math.max(0, Number(scan.walk_batches) || 0),
140
+ dirs: Math.max(0, Number(scan.dirs) || 0),
141
+ files: Math.max(0, Number(scan.files) || 0),
142
+ bytes_total: Math.max(0, Number(scan.bytes_total) || 0),
143
+ source_files: scan.source_files || {},
144
+ source_bytes: scan.source_bytes || {},
145
+ comparison_batches:
146
+ Math.max(0, Number(scan.comparison_batches) || 0),
147
+ comparison_files_verified:
148
+ Math.max(0, Number(scan.comparison_files_verified) || 0),
149
+ candidates: Math.max(0, Number(scan.candidates) || 0),
150
+ hash_work_files:
151
+ Math.max(0, Number(scan.hash_work_files) || 0),
152
+ hash_work_bytes: hashWorkBytes,
153
+ hash_files_completed:
154
+ Math.max(0, Number(scan.hash_files_completed) || 0),
155
+ hash_bytes_completed:
156
+ Math.max(0, Number(scan.hash_bytes_completed) || 0),
157
+ hash_progress_bytes: hashProgressBytes,
158
+ hash_progress_percent: hashWorkBytes
159
+ ? Math.floor((hashProgressBytes / hashWorkBytes) * 100)
160
+ : 0,
161
+ hash_total: Math.max(0, Number(scan.hash_total) || 0),
162
+ hashed: Math.max(0, Number(scan.hashed) || 0),
163
+ hash_bytes: Math.max(0, Number(scan.hash_bytes) || 0),
164
+ current_file: scan.current_file,
165
+ current_file_role: scan.current_file_role,
166
+ current_file_source_id: scan.current_file_source_id,
167
+ current_file_bytes: Number.isFinite(currentFileBytes)
168
+ ? currentFileBytes
169
+ : 0,
170
+ current_file_size: currentFileSize,
171
+ inode_reuses: Math.max(0, Number(scan.inode_reuses) || 0),
172
+ unstable_hashes: Math.max(0, Number(scan.unstable_hashes) || 0),
173
+ hash_failures: Math.max(0, Number(scan.hash_failures) || 0),
174
+ source_hash_failures: scan.source_hash_failures || {},
175
+ exclusions_count: Array.isArray(scan.exclusions)
176
+ ? scan.exclusions.length
177
+ : 0,
178
+ preview_duplicate_files:
179
+ Math.max(0, Number(preview.duplicate_files) || 0),
180
+ preview_savings_bytes: Math.max(0, Number(preview.bytes) || 0),
181
+ source_refresh_duration_ms: scan.source_refresh_duration_ms,
182
+ anchor_walk_duration_ms: scan.anchor_walk_duration_ms,
183
+ walk_duration_ms: scan.walk_duration_ms,
184
+ comparison_duration_ms: scan.comparison_duration_ms,
185
+ hash_wait_duration_ms: scan.hash_wait_duration_ms,
186
+ hash_duration_ms: scan.hash_duration_ms,
187
+ anchor_verify_duration_ms: scan.anchor_verify_duration_ms,
188
+ publish_duration_ms: scan.publish_duration_ms,
189
+ error: scan.error
190
+ }
191
+ const counters = this.diagnosticCounters(scan)
192
+ if (monitor.lastCounters && monitor.lastSampleAt < now) {
193
+ const sampleMs = now - monitor.lastSampleAt
194
+ details.sample_ms = sampleMs
195
+ details.files_per_second = Number((Math.max(
196
+ 0, counters.files - monitor.lastCounters.files
197
+ ) * 1000 / sampleMs).toFixed(2))
198
+ details.discovery_bytes_per_second = Math.round(Math.max(
199
+ 0, counters.discoveryBytes - monitor.lastCounters.discoveryBytes
200
+ ) * 1000 / sampleMs)
201
+ details.hash_bytes_per_second = Math.round(Math.max(
202
+ 0, counters.hashBytes - monitor.lastCounters.hashBytes
203
+ ) * 1000 / sampleMs)
204
+ }
205
+ monitor.lastCounters = counters
206
+ monitor.lastSampleAt = now
207
+ try {
208
+ const cpu = process.cpuUsage()
209
+ if (monitor.cpuUsage && Number(details.sample_ms) > 0) {
210
+ const user = Math.max(0, cpu.user - monitor.cpuUsage.user)
211
+ const system = Math.max(0, cpu.system - monitor.cpuUsage.system)
212
+ details.process_cpu_user_delta_ms =
213
+ Number((user / 1000).toFixed(2))
214
+ details.process_cpu_system_delta_ms =
215
+ Number((system / 1000).toFixed(2))
216
+ const sampleMs = Math.max(1, Number(details.sample_ms) ||
217
+ now - monitor.startedAt)
218
+ details.process_cpu_percent =
219
+ Number((((user + system) / 1000) * 100 / sampleMs).toFixed(2))
220
+ }
221
+ monitor.cpuUsage = cpu
222
+ const memory = process.memoryUsage()
223
+ details.process_rss_bytes = memory.rss
224
+ details.process_heap_used_bytes = memory.heapUsed
225
+ } catch (_) {}
226
+ return details
227
+ }
228
+
229
+ sampleScanDiagnostics(options = {}) {
230
+ const monitor = this.scanDiagnostics
231
+ if (!monitor) return null
232
+ let scan
233
+ try {
234
+ scan = typeof this.vault.scanStatus === "function"
235
+ ? this.vault.scanStatus()
236
+ : null
237
+ } catch (error) {
238
+ this.log("scan-progress-error", {
239
+ app: monitor.app,
240
+ scope_id: monitor.scopeId,
241
+ message: error && error.message ? error.message : String(error)
242
+ })
243
+ return null
244
+ }
245
+ if (!scan) return null
246
+ const now = this.clock()
247
+ const marker = this.diagnosticMarker(scan)
248
+ const changed = marker !== monitor.lastMarker
249
+ const previousUnchanged = now - monitor.lastChangedAt
250
+ if (changed) {
251
+ if (monitor.stalled) {
252
+ this.log("scan-progress-resumed", {
253
+ app: monitor.app,
254
+ scope_id: monitor.scopeId,
255
+ phase: scan.phase,
256
+ stage: scan.stage,
257
+ stalled_ms: Math.max(0, previousUnchanged)
258
+ })
259
+ }
260
+ monitor.lastMarker = marker
261
+ monitor.lastChangedAt = now
262
+ monitor.stalled = false
263
+ }
264
+ const stage = scan.stage || scan.phase || "unknown"
265
+ if (monitor.lastStage && stage !== monitor.lastStage) {
266
+ this.log("scan-stage-changed", {
267
+ app: monitor.app,
268
+ scope_id: monitor.scopeId,
269
+ from: monitor.lastStage,
270
+ to: stage,
271
+ previous_stage_observed_ms: Math.max(
272
+ 0, now - monitor.lastStageObservedAt)
273
+ })
274
+ monitor.lastStageObservedAt = now
275
+ }
276
+ monitor.lastStage = stage
277
+ const details = this.diagnosticDetails(monitor, scan, now)
278
+ if (!options.final) this.log("scan-progress", details)
279
+ if (!monitor.stalled && details.progress_unchanged_ms >=
280
+ this.scanStallThresholdMs && (scan.active || scan.pending)) {
281
+ monitor.stalled = true
282
+ this.log("scan-progress-stalled", details)
283
+ }
284
+ return details
285
+ }
286
+
287
+ startScanDiagnostics(app, scopeId) {
288
+ this.stopScanDiagnostics()
289
+ const now = this.clock()
290
+ let cpuUsage = null
291
+ try { cpuUsage = process.cpuUsage() } catch (_) {}
292
+ this.scanDiagnostics = {
293
+ app,
294
+ scopeId,
295
+ startedAt: now,
296
+ lastChangedAt: now,
297
+ lastMarker: null,
298
+ lastStage: null,
299
+ lastStageObservedAt: now,
300
+ lastCounters: null,
301
+ lastSampleAt: now,
302
+ cpuUsage,
303
+ stalled: false,
304
+ timer: null
305
+ }
306
+ const version = this.vault.kernel && this.vault.kernel.version
307
+ let cpuCount = null
308
+ let cpuModel = null
309
+ let totalMemory = null
310
+ let osRelease = null
311
+ try {
312
+ const cpus = os.cpus()
313
+ cpuCount = cpus.length
314
+ cpuModel = cpus[0] && cpus[0].model
315
+ totalMemory = os.totalmem()
316
+ osRelease = os.release()
317
+ } catch (_) {}
318
+ this.log("scan-diagnostics-started", {
319
+ app,
320
+ scope_id: scopeId,
321
+ progress_interval_ms: this.scanProgressIntervalMs,
322
+ stall_threshold_ms: this.scanStallThresholdMs,
323
+ platform: this.vault.kernel && this.vault.kernel.platform ||
324
+ process.platform,
325
+ arch: process.arch,
326
+ os_release: osRelease,
327
+ cpu_count: cpuCount,
328
+ cpu_model: cpuModel,
329
+ total_memory_bytes: totalMemory,
330
+ node_version: process.versions && process.versions.node,
331
+ electron_version: process.versions && process.versions.electron,
332
+ pinokiod_version: version && version.pinokiod,
333
+ process_pid: process.pid,
334
+ threshold_bytes: this.sizeThreshold,
335
+ entry_batch_size: ENTRY_BATCH_SIZE,
336
+ directory_concurrency: this.vault.scanner
337
+ ? this.vault.scanner.dirConcurrency
338
+ : DIR_CONCURRENCY,
339
+ stat_concurrency: this.vault.scanner
340
+ ? this.vault.scanner.statConcurrency
341
+ : STAT_CONCURRENCY,
342
+ hash_read_size: HASH_READ_SIZE,
343
+ hash_progress_interval_ms: HASH_PROGRESS_INTERVAL_MS,
344
+ hash_inactivity_timeout_ms:
345
+ Number(this.vault.hashInactivityMs) || HASH_INACTIVITY_MS
346
+ })
347
+ this.logFilesystemDiagnostics(app, scopeId)
348
+ this.sampleScanDiagnostics()
349
+ const timer = setInterval(() => {
350
+ this.sampleScanDiagnostics()
351
+ }, this.scanProgressIntervalMs)
352
+ if (typeof timer.unref === "function") timer.unref()
353
+ this.scanDiagnostics.timer = timer
354
+ }
355
+
356
+ finishScanDiagnostics(app) {
357
+ const monitor = this.scanDiagnostics
358
+ if (!monitor || monitor.app !== app) return {}
359
+ if (monitor.timer) clearInterval(monitor.timer)
360
+ const details = this.sampleScanDiagnostics({ final: true }) || {}
361
+ this.scanDiagnostics = null
362
+ return details
363
+ }
364
+
365
+ stopScanDiagnostics() {
366
+ const monitor = this.scanDiagnostics
367
+ if (monitor && monitor.timer) clearInterval(monitor.timer)
368
+ this.scanDiagnostics = null
369
+ }
370
+
371
+ logFilesystemDiagnostics(app, scopeId) {
372
+ if (typeof fs.promises.statfs !== "function") return
373
+ let source
374
+ try {
375
+ source = this.sourceForApp(app)
376
+ } catch (_) {
377
+ return
378
+ }
379
+ if (!source || !source.root) return
380
+ fs.promises.statfs(source.root).then((stat) => {
381
+ const blockSize = Math.max(0, Number(stat.bsize) || 0)
382
+ const bytes = (blocks) => Math.max(
383
+ 0, Number(blocks) || 0) * blockSize
384
+ this.log("scan-filesystem", {
385
+ app,
386
+ scope_id: scopeId,
387
+ source_device: source.dev,
388
+ filesystem_type: stat.type,
389
+ block_size: blockSize,
390
+ total_bytes: bytes(stat.blocks),
391
+ free_bytes: bytes(stat.bfree),
392
+ available_bytes: bytes(stat.bavail),
393
+ total_file_nodes: stat.files,
394
+ free_file_nodes: stat.ffree
395
+ })
396
+ }).catch((error) => {
397
+ this.log("scan-filesystem-error", {
398
+ app,
399
+ scope_id: scopeId,
400
+ code: error && error.code,
401
+ error_name: error && error.name
402
+ })
403
+ })
404
+ }
405
+
52
406
  appForLaunchPath(launchPath) {
53
407
  if (typeof launchPath !== "string" || !launchPath) return null
54
408
  const apiRoot = path.resolve(this.vault.kernel.homedir, "api")
@@ -515,6 +869,7 @@ class AutomaticScans {
515
869
  this.waitForBusyWork()
516
870
  } else {
517
871
  this.log("scan-started", { app: entry.app, scope_id: scopeId })
872
+ this.startScanDiagnostics(entry.app, scopeId)
518
873
  }
519
874
  }
520
875
 
@@ -743,13 +1098,14 @@ class AutomaticScans {
743
1098
  if (this.active && this.active.app === app) this.active = null
744
1099
  const reason = this.cancelReasons.get(app)
745
1100
  this.cancelReasons.delete(app)
746
- this.log("scan-finished", {
1101
+ const diagnostics = this.finishScanDiagnostics(app)
1102
+ this.log("scan-finished", Object.assign({}, diagnostics, {
747
1103
  app,
748
1104
  scope_id: scopeId,
749
1105
  outcome: result && result.outcome,
750
1106
  cancel_reason: reason,
751
1107
  error: error && error.message ? error.message : error
752
- })
1108
+ }))
753
1109
  try {
754
1110
  const entry = this.entries.get(app)
755
1111
  if (reason === "paused" || reason === "manual-mode" ||
@@ -1,5 +1,7 @@
1
1
  const CANDIDATE_SIZE_BASE = process.platform === "win32" ? 1024 : 1000
2
2
  const SIZE_THRESHOLD = 100 * CANDIDATE_SIZE_BASE ** 2
3
+ const HASH_READ_SIZE = 1024 * 1024
4
+ const HASH_PROGRESS_INTERVAL_MS = 1000
3
5
  const CANDIDATE_SIZE_OPTIONS = [0]
4
6
  .concat([1, 10, 50, 100, 500].map((value) => value * CANDIDATE_SIZE_BASE ** 2))
5
7
  .concat(CANDIDATE_SIZE_BASE ** 3)
@@ -14,5 +16,7 @@ module.exports = {
14
16
  ENTRY_BATCH_SIZE: 256,
15
17
  DIR_CONCURRENCY: 8,
16
18
  STAT_CONCURRENCY: 32,
17
- HASH_INACTIVITY_MS: 120 * 1000
19
+ HASH_INACTIVITY_MS: 120 * 1000,
20
+ HASH_READ_SIZE,
21
+ HASH_PROGRESS_INTERVAL_MS
18
22
  }
@@ -1,13 +1,14 @@
1
1
  const { parentPort } = require('worker_threads')
2
2
  const crypto = require('crypto')
3
3
  const fs = require('fs')
4
+ const {
5
+ HASH_READ_SIZE,
6
+ HASH_PROGRESS_INTERVAL_MS
7
+ } = require('./constants')
4
8
 
5
9
  // Large model files otherwise generate a very high number of 64 KiB stream
6
10
  // events. This changes only the read granularity; every byte still feeds the
7
11
  // same sha256 digest, one file at a time.
8
- const HASH_READ_SIZE = 1024 * 1024
9
- const HASH_PROGRESS_INTERVAL_MS = 1000
10
-
11
12
  parentPort.on('message', ({ id, filePath }) => {
12
13
  const hash = crypto.createHash('sha256')
13
14
  let size = 0
@@ -3372,6 +3372,7 @@ class Vault {
3372
3372
  const state = pending
3373
3373
  ? Object.assign(this.sweeper.idleState(), {
3374
3374
  phase: "queued",
3375
+ stage: "queued",
3375
3376
  scope_id: this.scanScopeId
3376
3377
  })
3377
3378
  : this.sweeper.state
@@ -3385,6 +3386,12 @@ class Vault {
3385
3386
  current_file_size: this.sweeper.currentHash
3386
3387
  ? this.sweeper.currentHash.size
3387
3388
  : null,
3389
+ current_file_source_id: this.sweeper.currentHash
3390
+ ? this.sweeper.currentHash.source_id
3391
+ : null,
3392
+ current_file_role: this.sweeper.currentHash
3393
+ ? this.sweeper.currentHash.role
3394
+ : null,
3388
3395
  pending,
3389
3396
  error: this.scanError
3390
3397
  })
@@ -22,9 +22,20 @@ class Sweeper {
22
22
  return {
23
23
  active: false,
24
24
  phase: "idle",
25
+ stage: "idle",
26
+ stage_started: null,
27
+ previous_stage: null,
28
+ previous_stage_duration_ms: null,
29
+ last_progress_at: null,
30
+ current_directory: null,
31
+ anchor_files: 0,
32
+ anchor_bytes: 0,
25
33
  dirs: 0,
26
34
  files: 0,
27
35
  bytes_total: 0,
36
+ walk_batches: 0,
37
+ comparison_batches: 0,
38
+ comparison_files_verified: 0,
28
39
  source_bytes: {},
29
40
  source_files: {},
30
41
  source_hash_failures: {},
@@ -50,12 +61,34 @@ class Sweeper {
50
61
  },
51
62
  started: null,
52
63
  duration_ms: null,
64
+ source_refresh_duration_ms: null,
65
+ anchor_walk_duration_ms: null,
53
66
  walk_duration_ms: null,
67
+ comparison_duration_ms: null,
54
68
  hash_wait_duration_ms: null,
55
- hash_duration_ms: 0
69
+ hash_duration_ms: 0,
70
+ anchor_verify_duration_ms: null,
71
+ publish_duration_ms: null
56
72
  }
57
73
  }
58
74
 
75
+ setStage(stage) {
76
+ const now = Date.now()
77
+ if (this.state.stage && this.state.stage !== stage &&
78
+ Number.isFinite(this.state.stage_started)) {
79
+ this.state.previous_stage = this.state.stage
80
+ this.state.previous_stage_duration_ms = Math.max(
81
+ 0, now - this.state.stage_started)
82
+ }
83
+ this.state.stage = stage
84
+ this.state.stage_started = now
85
+ this.state.last_progress_at = now
86
+ }
87
+
88
+ markProgress() {
89
+ this.state.last_progress_at = Date.now()
90
+ }
91
+
59
92
  cancel() {
60
93
  if (!this.state.active) return false
61
94
  this.cancelRequested = true
@@ -89,6 +122,7 @@ class Sweeper {
89
122
  started: Date.now(),
90
123
  scope_id: scopeId
91
124
  })
125
+ this.setStage("starting")
92
126
 
93
127
  const registry = this.vault.registry
94
128
  const runId = await registry.beginScan(scopeId)
@@ -96,7 +130,12 @@ class Sweeper {
96
130
  let fatalError = null
97
131
  let affectedApps = []
98
132
  try {
133
+ this.setStage("refreshing_sources")
134
+ const sourceRefreshStarted = Date.now()
99
135
  await this.vault.refreshSources()
136
+ this.state.source_refresh_duration_ms =
137
+ Date.now() - sourceRefreshStarted
138
+ this.markProgress()
100
139
  if (!scopeId) {
101
140
  const unavailable = this.vault.sources().find((source) =>
102
141
  source.kind === "external" &&
@@ -113,9 +152,13 @@ class Sweeper {
113
152
  }
114
153
 
115
154
  const anchorStores = this.anchorStoresForScope(scopeId)
155
+ this.setStage("scanning_anchors")
156
+ const anchorWalkStarted = Date.now()
116
157
  await this.stageAnchors(runId, anchorStores)
158
+ this.state.anchor_walk_duration_ms = Date.now() - anchorWalkStarted
117
159
  this.checkpoint()
118
160
 
161
+ this.setStage("discovering_files")
119
162
  const walkStarted = Date.now()
120
163
  for (const source of scanRoots) {
121
164
  if (!Object.prototype.hasOwnProperty.call(
@@ -127,20 +170,34 @@ class Sweeper {
127
170
  await this.walk(source.root, runId, source.source_id)
128
171
  this.checkpoint()
129
172
  }
173
+ this.state.current_directory = null
130
174
  this.state.walk_duration_ms = Date.now() - walkStarted
175
+ this.setStage("staging_discovery_results")
131
176
  await registry.stageExclusions(runId, this.exclusionList())
132
177
  if (scopeId) {
178
+ const comparisonStarted = Date.now()
179
+ this.setStage("staging_comparisons")
133
180
  const comparisons = await registry.stageComparisonFiles(
134
181
  runId, this.publicationSourceIds(scopeId))
135
182
  this.applyHashWork(comparisons && comparisons.work)
183
+ this.setStage("verifying_comparisons")
136
184
  await this.verifyComparisonFiles(runId)
185
+ this.state.comparison_duration_ms =
186
+ Date.now() - comparisonStarted
137
187
  }
138
188
 
139
189
  this.state.phase = "hashing"
190
+ this.setStage("hashing_candidates")
140
191
  const hashStarted = Date.now()
141
192
  await this.hashCandidates(runId)
193
+ this.setStage("staging_hash_results")
142
194
  await registry.stageExclusions(runId, this.exclusionList())
195
+ this.setStage("verifying_anchors")
196
+ const anchorVerifyStarted = Date.now()
143
197
  await this.verifyCandidateAnchors(runId)
198
+ this.state.anchor_verify_duration_ms =
199
+ Date.now() - anchorVerifyStarted
200
+ this.setStage("staging_verification_results")
144
201
  await registry.stageExclusions(runId, this.exclusionList())
145
202
  this.state.hash_wait_duration_ms = Date.now() - hashStarted
146
203
  this.checkpoint()
@@ -149,6 +206,7 @@ class Sweeper {
149
206
  ? "completed_with_exclusions"
150
207
  : "complete"
151
208
  this.state.phase = "publishing"
209
+ this.setStage("publishing")
152
210
  this.state.duration_ms = Date.now() - this.state.started
153
211
  const metadata = this.scanMetadata(scopeId, outcome)
154
212
  const stores = anchorStores
@@ -160,12 +218,15 @@ class Sweeper {
160
218
  can_link: store.mode !== "copy",
161
219
  root: store.root
162
220
  }))
221
+ const publishStarted = Date.now()
163
222
  const publication = await registry.publishScan(
164
223
  runId,
165
224
  this.publicationSourceIds(scopeId),
166
225
  metadata,
167
226
  stores
168
227
  )
228
+ this.state.publish_duration_ms = Date.now() - publishStarted
229
+ this.markProgress()
169
230
  affectedApps = publication && Array.isArray(publication.affected_apps)
170
231
  ? publication.affected_apps
171
232
  : []
@@ -178,9 +239,11 @@ class Sweeper {
178
239
  }
179
240
  } finally {
180
241
  this.currentHash = null
242
+ this.state.current_directory = null
181
243
  this.state.active = false
182
244
  this.state.phase = outcome
183
245
  this.state.duration_ms = Date.now() - this.state.started
246
+ this.setStage(outcome)
184
247
  this.clearPreview()
185
248
  }
186
249
 
@@ -223,9 +286,15 @@ class Sweeper {
223
286
  hash_failures: this.state.hash_failures,
224
287
  candidate_min_bytes: this.vault.sizeThreshold,
225
288
  duration_ms: Date.now() - this.state.started,
289
+ source_refresh_duration_ms:
290
+ this.state.source_refresh_duration_ms || 0,
291
+ anchor_walk_duration_ms: this.state.anchor_walk_duration_ms || 0,
226
292
  walk_duration_ms: this.state.walk_duration_ms || 0,
293
+ comparison_duration_ms: this.state.comparison_duration_ms || 0,
227
294
  hash_wait_duration_ms: this.state.hash_wait_duration_ms || 0,
228
- hash_duration_ms: this.state.hash_duration_ms || 0
295
+ hash_duration_ms: this.state.hash_duration_ms || 0,
296
+ anchor_verify_duration_ms:
297
+ this.state.anchor_verify_duration_ms || 0
229
298
  }
230
299
  }
231
300
 
@@ -334,9 +403,14 @@ class Sweeper {
334
403
  onError: (error, filePath, reason = null) =>
335
404
  this.recordExclusion(error, filePath, null, reason),
336
405
  onEntries: async (anchors) => {
406
+ this.state.anchor_files += anchors.length
407
+ this.state.anchor_bytes += anchors.reduce((total, anchor) =>
408
+ total + (Math.max(0, Number(anchor.size) || 0)), 0)
409
+ this.markProgress()
337
410
  const staged = await this.vault.registry.stageAnchors(
338
411
  runId, anchors)
339
412
  this.applyHashWork(staged && staged.work)
413
+ this.markProgress()
340
414
  }
341
415
  })
342
416
  }
@@ -352,6 +426,9 @@ class Sweeper {
352
426
  }
353
427
  const observations = await this.vault.scanner.validateSnapshots(entries)
354
428
  await this.vault.registry.resolveComparisonFiles(runId, observations)
429
+ this.state.comparison_batches += 1
430
+ this.state.comparison_files_verified += entries.length
431
+ this.markProgress()
355
432
  }
356
433
  }
357
434
 
@@ -361,7 +438,12 @@ class Sweeper {
361
438
  skipDirectory: (full) => this.vault.isStorageRoot(full),
362
439
  onError: (error, filePath) =>
363
440
  this.recordExclusion(error, filePath, preferredSourceId),
364
- onBatch: async ({ files, directories }) => {
441
+ onBatch: async ({ files, directories, currentDirectory }) => {
442
+ this.state.current_directory = currentDirectory
443
+ ? path.relative(root, currentDirectory) || "."
444
+ : this.state.current_directory
445
+ this.markProgress()
446
+ this.state.walk_batches += 1
365
447
  this.state.dirs += directories
366
448
  const entries = files.map((file) =>
367
449
  this.considerStat(file.path, file.stat, preferredSourceId))
@@ -371,6 +453,7 @@ class Sweeper {
371
453
  this.state.candidates += Number(staged && staged.changes) || 0
372
454
  this.applyPreview(staged && staged.preview)
373
455
  this.applyHashWork(staged && staged.work)
456
+ this.markProgress()
374
457
  }
375
458
  })
376
459
  }
@@ -421,6 +504,7 @@ class Sweeper {
421
504
  runId, candidate.dev, candidate.ino, candidate.reusable_hash)
422
505
  this.state.inode_reuses += updated.changes
423
506
  this.applyPreview(updated.preview)
507
+ this.markProgress()
424
508
  continue
425
509
  }
426
510
  await this.hashCandidateRoutes(runId, candidate)
@@ -439,14 +523,18 @@ class Sweeper {
439
523
  this.currentHash = {
440
524
  path: candidate.path,
441
525
  size: candidate.size,
442
- bytes: 0
526
+ bytes: 0,
527
+ source_id: candidate.source_id || null,
528
+ role: candidate.comparison_only ? "comparison" : "candidate"
443
529
  }
530
+ this.markProgress()
444
531
  try {
445
532
  const verified = await this.vault.scanner.hashStable(candidate, {
446
533
  onProgress: (bytes) => {
447
534
  if (this.currentHash &&
448
535
  this.currentHash.path === candidate.path) {
449
536
  this.currentHash.bytes = bytes
537
+ this.markProgress()
450
538
  }
451
539
  }
452
540
  })
@@ -476,6 +564,7 @@ class Sweeper {
476
564
  this.state.hash_bytes += verified.result.size
477
565
  this.state.inode_reuses += Math.max(0, updated.changes - 1)
478
566
  this.applyPreview(updated.preview)
567
+ this.markProgress()
479
568
  candidate = null
480
569
  deferCompletion = false
481
570
  } catch (error) {
@@ -513,14 +602,18 @@ class Sweeper {
513
602
  this.currentHash = {
514
603
  path: anchor.path,
515
604
  size: anchor.size,
516
- bytes: 0
605
+ bytes: 0,
606
+ source_id: null,
607
+ role: "anchor"
517
608
  }
609
+ this.markProgress()
518
610
  try {
519
611
  const verified = await this.vault.scanner.hashStable(anchor, {
520
612
  onProgress: (bytes) => {
521
613
  if (this.currentHash &&
522
614
  this.currentHash.path === anchor.path) {
523
615
  this.currentHash.bytes = bytes
616
+ this.markProgress()
524
617
  }
525
618
  }
526
619
  })
@@ -528,6 +621,7 @@ class Sweeper {
528
621
  await this.vault.registry.markAnchorChecked(
529
622
  runId, anchor, verified.result.hash)
530
623
  completeWork = true
624
+ this.markProgress()
531
625
  } else {
532
626
  const retry =
533
627
  await this.vault.registry.markAnchorVerificationFailed(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.54",
3
+ "version": "8.0.55",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12131,11 +12131,6 @@ const rerenderMenuSection = (container, html) => {
12131
12131
  let shell = target.closest("[data-shell]")
12132
12132
  if (shell) {
12133
12133
  let shell_id = shell.getAttribute("data-shell")
12134
- n.Noty({
12135
- text: `stopping shell`,
12136
- silent: true,
12137
- timeout: 2000
12138
- })
12139
12134
  let socket = new Socket()
12140
12135
  socket.run({
12141
12136
  method: "kernel.bin.shell_kill",
@@ -12146,11 +12141,6 @@ const rerenderMenuSection = (container, html) => {
12146
12141
  console.log("packet", packet)
12147
12142
  if (packet.type === "result") {
12148
12143
  socket.close()
12149
- n.Noty({
12150
- timeout: 2000,
12151
- text: `stopped`,
12152
- silent: true
12153
- })
12154
12144
  console.log("Refresh 4")
12155
12145
  refresh(true)
12156
12146
  }
@@ -12163,11 +12153,6 @@ const rerenderMenuSection = (container, html) => {
12163
12153
  target.querySelector("i").className = "fa-solid fa-check"
12164
12154
  } catch (e) {
12165
12155
  }
12166
- n.Noty({
12167
- text: `stopping script`,
12168
- silent: true,
12169
- timeout: 2000
12170
- })
12171
12156
 
12172
12157
  let socket = new Socket()
12173
12158
  socket.run({
@@ -12179,11 +12164,6 @@ const rerenderMenuSection = (container, html) => {
12179
12164
  if (packet.type === "result") {
12180
12165
  console.log("is result")
12181
12166
  socket.close()
12182
- n.Noty({
12183
- timeout: 2000,
12184
- text: `stopped`,
12185
- silent: true
12186
- })
12187
12167
  console.log("Refresh 4")
12188
12168
  refresh(true)
12189
12169
  }
@@ -12196,11 +12176,6 @@ const rerenderMenuSection = (container, html) => {
12196
12176
  target.querySelector("i").className = "fa-solid fa-check"
12197
12177
  } catch (e) {
12198
12178
  }
12199
- n.Noty({
12200
- text: `stopping ${src}`,
12201
- silent: true,
12202
- timeout: 2000
12203
- })
12204
12179
  console.log("src", src)
12205
12180
 
12206
12181
  let socket = new Socket()
@@ -12212,11 +12187,6 @@ const rerenderMenuSection = (container, html) => {
12212
12187
  }, (packet) => {
12213
12188
  if (packet.type === "result") {
12214
12189
  socket.close()
12215
- n.Noty({
12216
- timeout: 2000,
12217
- text: `stopped`,
12218
- silent: true
12219
- })
12220
12190
  console.log("Refresh 4")
12221
12191
  refresh(true)
12222
12192
  }
@@ -1698,6 +1698,106 @@ describe("automatic app scans", () => {
1698
1698
  await close(vault)
1699
1699
  })
1700
1700
 
1701
+ test("diagnostics report progress, stalls, resumed work, and final state", () => {
1702
+ let now = 1000
1703
+ let scan = {
1704
+ active: true,
1705
+ pending: false,
1706
+ phase: "discovering",
1707
+ stage: "discovering_files",
1708
+ stage_started: now,
1709
+ last_progress_at: now,
1710
+ started: now,
1711
+ scope_id: "app:demo",
1712
+ dirs: 1,
1713
+ files: 2,
1714
+ bytes_total: 2048,
1715
+ source_files: { "app:demo": 2 },
1716
+ source_bytes: { "app:demo": 2048 },
1717
+ exclusions: [],
1718
+ preview: { duplicate_files: 0, bytes: 0 }
1719
+ }
1720
+ const automatic = new AutomaticScans({
1721
+ kernel: { homedir: "/pinokio", platform: "win32" },
1722
+ scanner: { dirConcurrency: 8, statConcurrency: 32 },
1723
+ hashInactivityMs: 120000,
1724
+ scanStatus: () => scan
1725
+ })
1726
+ automatic.clock = () => now
1727
+ automatic.scanProgressIntervalMs = 60000
1728
+ automatic.scanStallThresholdMs = 100
1729
+ const events = []
1730
+ automatic.log = (event, details) => events.push({ event, details })
1731
+
1732
+ automatic.startScanDiagnostics("demo", "app:demo")
1733
+ now += 50
1734
+ scan = Object.assign({}, scan, {
1735
+ dirs: 4,
1736
+ files: 10,
1737
+ bytes_total: 10240,
1738
+ last_progress_at: now
1739
+ })
1740
+ automatic.sampleScanDiagnostics()
1741
+ now += 100
1742
+ automatic.sampleScanDiagnostics()
1743
+
1744
+ const stalled = events.find((entry) =>
1745
+ entry.event === "scan-progress-stalled")
1746
+ assert.ok(stalled)
1747
+ assert.equal(stalled.details.stage, "discovering_files")
1748
+ assert.equal(stalled.details.files, 10)
1749
+ assert.equal(stalled.details.progress_unchanged_ms, 100)
1750
+
1751
+ now += 10
1752
+ scan = Object.assign({}, scan, {
1753
+ phase: "hashing",
1754
+ stage: "hashing_candidates",
1755
+ stage_started: now,
1756
+ last_progress_at: now,
1757
+ hash_work_files: 2,
1758
+ hash_work_bytes: 8192,
1759
+ current_file: "model.bin",
1760
+ current_file_role: "candidate",
1761
+ current_file_source_id: "app:demo",
1762
+ current_file_bytes: 1024,
1763
+ current_file_size: 4096
1764
+ })
1765
+ automatic.sampleScanDiagnostics()
1766
+
1767
+ assert.ok(events.some((entry) =>
1768
+ entry.event === "scan-progress-resumed"))
1769
+ assert.ok(events.some((entry) =>
1770
+ entry.event === "scan-stage-changed" &&
1771
+ entry.details.from === "discovering_files" &&
1772
+ entry.details.to === "hashing_candidates"))
1773
+ const hashing = events.filter((entry) =>
1774
+ entry.event === "scan-progress").at(-1).details
1775
+ assert.equal(hashing.hash_progress_bytes, 1024)
1776
+ assert.equal(hashing.hash_progress_percent, 12)
1777
+ assert.equal(hashing.current_file, "model.bin")
1778
+ assert.ok(hashing.hash_bytes_per_second > 0)
1779
+
1780
+ now += 20
1781
+ scan = Object.assign({}, scan, {
1782
+ active: false,
1783
+ phase: "cancelled",
1784
+ stage: "cancelled",
1785
+ previous_stage: "hashing_candidates",
1786
+ previous_stage_duration_ms: 20,
1787
+ current_file: null,
1788
+ current_file_role: null,
1789
+ current_file_source_id: null,
1790
+ current_file_bytes: null,
1791
+ current_file_size: null
1792
+ })
1793
+ const final = automatic.finishScanDiagnostics("demo")
1794
+
1795
+ assert.equal(final.phase, "cancelled")
1796
+ assert.equal(final.previous_stage, "hashing_candidates")
1797
+ assert.equal(final.previous_stage_duration_ms, 20)
1798
+ assert.equal(automatic.scanDiagnostics, null)
1799
+ })
1800
+
1701
1801
  test("existing lifecycle results are unchanged while Vault observes them", () => {
1702
1802
  const calls = []
1703
1803
  const fakeKernel = {
@@ -513,12 +513,15 @@ describe("Save Space scans", () => {
513
513
 
514
514
  try {
515
515
  assert.equal(progress.phase, "hashing")
516
+ assert.equal(progress.stage, "hashing_candidates")
516
517
  assert.equal(progress.hash_work_files, 2)
517
518
  assert.equal(progress.hash_work_bytes, contents.length * 2)
518
519
  assert.equal(progress.hash_files_completed, 0)
519
520
  assert.equal(progress.hash_bytes_completed, 0)
520
521
  assert.equal(progress.current_file_bytes, 1024)
521
522
  assert.equal(progress.current_file_size, contents.length)
523
+ assert.equal(progress.current_file_role, "candidate")
524
+ assert.match(progress.current_file_source_id, /^app:/)
522
525
  } finally {
523
526
  release()
524
527
  }
@@ -528,6 +531,8 @@ describe("Save Space scans", () => {
528
531
  vault.scanStatus().hash_bytes_completed,
529
532
  contents.length * 2
530
533
  )
534
+ assert.equal(vault.scanStatus().previous_stage, "publishing")
535
+ assert.ok(vault.scanStatus().publish_duration_ms >= 0)
531
536
  })
532
537
 
533
538
  test("an anchor sharing a scanned inode is not counted as a second read", async () => {